# Daytona Documentation
# https://www.daytona.io/docs
# Generated on: 2026-06-16
# Daytona Documentation
Daytona is an open-source, secure and elastic infrastructure for running AI-generated code. Daytona provides full composable computers — [sandboxes](https://www.daytona.io/docs/en/sandboxes.md) — with complete isolation, a dedicated kernel, filesystem, network stack, 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, massive parallelization, and unlimited persistence, sandboxes deliver consistent, predictable environments for agent workflows.
Agents and developers interact with sandboxes programmatically using the Daytona SDKs, API, and CLI. Operations span sandbox lifecycle management, filesystem operations, process and code execution, and runtime configuration. Our stateful environment snapshots enable persistent agent operations across sessions, making Daytona the ideal foundation for AI agent architectures.
- **Daytona SDKs**: [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Python](https://www.daytona.io/docs/en/python-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), [Java](https://www.daytona.io/docs/en/java-sdk.md)
- **Daytona API**: [RESTful API](https://www.daytona.io/docs/en/tools/api.md#daytona) ([OpenAPI spec](https://www.daytona.io/docs/openapi.json)), [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) ([OpenAPI spec](https://www.daytona.io/docs/toolbox-openapi.json))
- **Daytona CLI**: [Mac/Linux](https://www.daytona.io/docs/en/getting-started.md#cli), [Windows](https://www.daytona.io/docs/en/getting-started.md#cli), [Reference](https://www.daytona.io/docs/en/tools/cli.md)
:::tip
For faster development with AI agents and assistants, use our LLMs context files: [llms.txt](https://www.daytona.io/docs/llms.txt) and [llms-full.txt](https://www.daytona.io/docs/llms-full.txt), [agent skills](https://github.com/daytona/skills), and markdown pages by appending `.md` to URLs.
```text
npx skills add https://github.com/daytona/skills --skill daytona
```
:::
## 1. Create an account
Open the [Daytona Dashboard ↗](https://app.daytona.io/) to create your account. Daytona supports account creation using an email and password, or by connecting your Google or GitHub account.
## 2. Obtain an API key
Generate an API key from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/keys) or using the [Daytona API](https://www.daytona.io/docs/en/tools/api.md#daytona/tag/api-keys/POST/api-keys) to authenticate SDK requests and access Daytona services. Daytona supports multiple options to configure your environment and API keys: [in code](https://www.daytona.io/docs/en/configuration.md#configuration-in-code), [environment variables](https://www.daytona.io/docs/en/configuration.md#environment-variables), [.env file](https://www.daytona.io/docs/en/configuration.md#env-file), and [default values](https://www.daytona.io/docs/en/configuration.md#default-values).
## 3. Install the SDK
Install the Daytona **Python**, **TypeScript**, **Ruby**, **Go**, or **Java** SDKs to interact with sandboxes.
```bash
pip install daytona
```
```bash
npm install @daytona/sdk
```
```bash
gem install daytona
```
```bash
go get github.com/daytonaio/daytona/libs/sdk-go
```
**Gradle**
Add the Daytona SDK dependency to your `build.gradle.kts`:
```kotlin
dependencies {
implementation("io.daytona:sdk:x.y.z")
}
```
**Maven**
Add the Daytona SDK dependency to your `pom.xml`:
```xml
io.daytona
sdk
x.y.z
```
## 4. Create a Sandbox
Create a sandbox to run your code securely in an isolated environment.
```python
# Import the Daytona SDK
from daytona import Daytona, DaytonaConfig
# Define the configuration
config = DaytonaConfig(api_key="YOUR_API_KEY") # Replace with your API key
# Initialize the Daytona client
daytona = Daytona(config)
# Create the Sandbox instance
sandbox = daytona.create()
```
```typescript
// Import the Daytona SDK
import { Daytona } from '@daytona/sdk'
// Initialize the Daytona client
const daytona = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Replace with your API key
// Create the Sandbox instance
const sandbox = await daytona.create()
```
```ruby
require 'daytona'
# Initialize the Daytona client
config = Daytona::Config.new(api_key: 'YOUR_API_KEY') # Replace with your API key
# Create the Daytona client
daytona = Daytona::Daytona.new(config)
# Create the Sandbox instance
sandbox = daytona.create
```
```go
package main
import (
"context"
"fmt"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
config := &types.DaytonaConfig{
APIKey: "YOUR_API_KEY", // Replace with your API key
}
client, _ := daytona.NewClientWithConfig(config)
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
fmt.Println(sandbox.ID)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.DaytonaConfig;
public class Main {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey("YOUR_API_KEY")
.build();
try (Daytona daytona = new Daytona(config)) {
Sandbox sandbox = daytona.create();
System.out.println(sandbox.getId());
}
}
}
```
```bash
curl https://app.daytona.io/api/sandbox \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
```
```shell
daytona create
```
## 5. Write and run code
Create a program that runs code inside a sandbox. The following snippets are examples of "Hello World" programs that run securely inside a sandbox.
```python
# Import the Daytona SDK
from daytona import Daytona, DaytonaConfig
# Define the configuration
config = DaytonaConfig(api_key="YOUR_API_KEY") # Replace with your API key
# Initialize the Daytona client
daytona = Daytona(config)
# Create the Sandbox instance
sandbox = daytona.create()
# Run the code securely inside the Sandbox
response = sandbox.process.code_run('print("Hello World")')
# Check the response
if response.exit_code != 0:
print(f"Error: {response.exit_code} {response.result}")
else:
print(response.result)
# Clean up
sandbox.delete()
```
```typescript
// Import the Daytona SDK
import { Daytona } from '@daytona/sdk'
// Initialize the Daytona client
const daytona = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Replace with your API key
// Create the Sandbox instance
const sandbox = await daytona.create({
language: 'typescript',
})
// Run the code securely inside the Sandbox
const response = await sandbox.process.codeRun('console.log("Hello World")')
// Check the response
if (response.exitCode !== 0) {
console.error(`Error: ${response.exitCode} ${response.result}`)
} else {
console.log(response.result)
}
// Clean up
await sandbox.delete()
```
```ruby
require 'daytona'
# Initialize the Daytona client
config = Daytona::Config.new(api_key: 'YOUR_API_KEY')
daytona = Daytona::Daytona.new(config)
# Create the Sandbox instance
sandbox = daytona.create
# Run the code securely inside the Sandbox
response = sandbox.process.code_run(code: 'print("Hello World")')
puts response.result
```
```go
// Import the Daytona SDK
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
// Define the configuration
config := &types.DaytonaConfig{
APIKey: "YOUR_API_KEY", // Replace with your API key
}
// Initialize the Daytona client
client, err := daytona.NewClientWithConfig(config)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Create the Sandbox instance
params := types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
},
}
sandbox, err := client.Create(ctx, params)
if err != nil {
log.Fatal(err)
}
// Run the code securely inside the Sandbox
result, err := sandbox.Process.ExecuteCommand(ctx, `echo "Hello World"`)
// Check the response
if err != nil {
log.Fatalf("Error: %v", err)
}
if result.ExitCode != 0 {
log.Printf("Error: %d %s", result.ExitCode, result.Result)
} else {
log.Println(result.Result)
}
// Clean up
sandbox.Delete(ctx)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.ExecuteResponse;
public class Main {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey("YOUR_API_KEY") // Replace with your API key
.build();
try (Daytona daytona = new Daytona(config)) {
// Create the Sandbox instance
Sandbox sandbox = daytona.create();
// Run the code securely inside the Sandbox
ExecuteResponse response = sandbox.getProcess().executeCommand("echo \"Hello World\"");
// Check the response
if (response.getExitCode() != 0) {
System.err.println("Error: " + response.getExitCode() + " " + response.getResult());
} else {
System.out.println(response.getResult());
}
// Clean up
sandbox.delete();
}
}
}
```
## Summary
By following the steps above, you successfully create a Daytona account, obtain an API key, install the SDK, create a sandbox, write code, and run it securely in a sandbox.
## Next steps
Use the following resources to interact with sandboxes:
- Learn more about Daytona with the [getting started](https://www.daytona.io/docs/en/getting-started.md) and [sandboxes](https://www.daytona.io/docs/en/sandboxes.md) guides
- View [examples](https://www.daytona.io/docs/en/getting-started.md#examples) for common sandbox operations and best practices
- Explore [guides](https://www.daytona.io/docs/en/guides.md) to connect Daytona with [Claude](https://www.daytona.io/docs/en/guides/claude.md), [OpenCode](https://www.daytona.io/docs/en/guides/opencode/opencode-web-agent.md), [Codex](https://www.daytona.io/docs/en/guides/codex/codex-sdk-interactive-terminal-sandbox.md), [LangChain](https://www.daytona.io/docs/en/guides/langchain/langchain-data-analysis.md) and more
# Architecture
Daytona provides **full composable computers** — [sandboxes](https://www.daytona.io/docs/en/sandboxes.md) — for AI agents. Daytona platform is organized into multiple plane components, each serving a specific purpose:
- [Interface plane](#interface-plane) provides client interfaces for interacting with Daytona
- [Control plane](#control-plane) orchestrates all sandbox operations
- [Compute plane](#compute-plane) runs and manages sandbox instances
### Interface plane
The interface plane provides client interfaces for users and agents to interact with Daytona. The following components are part of the interface plane and available to all users and agents:
- **SDK**: [Python](https://www.daytona.io/docs/en/python-sdk.md), [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), and [Java](https://www.daytona.io/docs/en/java-sdk.md) SDKs for programmatic access
- [CLI](https://www.daytona.io/docs/en/tools/cli.md): command-line interface for direct sandbox operations
- [Dashboard](https://app.daytona.io/dashboard/): web interface for visual sandbox management and monitoring
- [MCP](https://www.daytona.io/docs/en/mcp.md): Model Context Protocol server for AI tool integrations
- [SSH](https://www.daytona.io/docs/en/ssh-access.md): secure shell access to running sandboxes
### Control plane
The control plane is the central coordination layer of the Daytona platform. It receives all client requests, manages the full sandbox lifecycle, schedules sandboxes onto runners, and continuously reconciles states across the infrastructure. The control plane includes the following components:
- [API](#api) handles authentication, sandbox lifecycle management, and resource allocation
- [Proxy](#proxy) routes external traffic to sandboxes, enabling direct access to services
- [Snapshot builder](#snapshot-builder) builds and manages sandbox [snapshots](https://www.daytona.io/docs/en/snapshots.md)
- [Sandbox manager](#sandbox-manager) handles sandbox lifecycle management and state reconciliation
#### API
The API is a NestJS-based RESTful service that serves as the primary entry point for all platform operations, managing authentication, sandbox lifecycle, snapshots, volumes, and resource allocation. The [snapshot builder](#snapshot-builder) and [sandbox manager](#sandbox-manager) run as internal processes within the API. The API integrates the following internal services and components:
- **Redis** provides caching, session management, and distributed locking
- **PostgreSQL** serves as the primary persistent store for metadata and configuration
- **Auth0/OIDC provider** authenticates users and services via OpenID Connect. The API enforces organization-level multi-tenancy, where each sandbox, snapshot, and volume belongs to an organization, and access control is applied at the organization boundary
- **SMTP server** handles email delivery for organization invitations, account notifications, and alert messages
- [Sandbox manager](#sandbox-manager) schedules sandboxes onto runners, reconciles states, and enforces sandbox lifecycle management policies
- **PostHog** collects platform analytics and usage metrics for monitoring and improvement
To interact with sandboxes from the API, see the [API](https://www.daytona.io/docs/en/tools/api.md) and [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) references.
#### Proxy
The proxy is a dedicated HTTP proxy that routes external traffic to the correct sandbox using host-based routing. Each sandbox is reachable at `{port}-{sandboxId}.{proxy-domain}`, where the port maps to a service running inside the sandbox. The proxy resolves the target runner for a given sandbox, injects authentication headers, and forwards the request. It supports both HTTP and WebSocket protocols.
#### Snapshot builder
The snapshot builder is part of the API process and orchestrates the creation of sandbox [snapshots](https://www.daytona.io/docs/en/snapshots.md) from a Dockerfile or a pre-built image from a [container registry](#container-registry). It coordinates with runners to build or pull images, which are then pushed to an internal snapshot registry that implements the OCI distribution specification.
#### Sandbox manager
The sandbox manager is part of the API process and schedules sandboxes onto runners, reconciles states, and enforces [sandbox lifecycle management](https://www.daytona.io/docs/en/sandboxes.md#sandbox-lifecycle) policies.
### Compute plane
The compute plane is the infrastructure layer where sandboxes run. Sandboxes run on [runners](#sandbox-runners), compute nodes that host multiple sandboxes with dedicated resources and scale horizontally across shared or dedicated [regions](https://www.daytona.io/docs/en/regions.md). The compute plane consists of the following components:
- [Sandbox runners](#sandbox-runners) host sandboxes with dedicated resources
- [Sandbox daemon](#sandbox-daemon) provides code execution and environment access inside each sandbox
- [Snapshot store](#snapshot-store) stores sandbox snapshot images
- [Volumes](#volumes) provides persistent storage shared across sandboxes
#### Sandbox runners
Runners are compute nodes that power Daytona's compute plane, providing the underlying infrastructure for running sandbox workloads. Each runner polls the control plane API for jobs and executes sandbox operations: creating, starting, stopping, destroying, resizing, and backing up sandboxes. Runners interact with S3-compatible object storage for snapshot and volume data, and with the internal snapshot registry.
Each sandbox runs as an isolated instance with its own Linux namespaces for processes, network, filesystem mounts, and inter-process communication. Each runner allocates dedicated vCPU, RAM, and disk resources per sandbox.
#### Sandbox daemon
The sandbox daemon is a code execution agent that runs inside each sandbox. It exposes the [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox), providing direct access to the sandbox environment: file system and Git operations, process and code execution, computer use, log streaming, and terminal sessions.
#### Snapshot store
The snapshot store is an internal OCI-compliant registry that stores sandbox snapshot images using the OCI distribution specification. Runners pull snapshot images from this store when creating new sandboxes. The store uses S3-compatible object storage as its backend.
#### Volumes
[Volumes](https://www.daytona.io/docs/en/volumes.md) provide persistent storage that can be shared across sandboxes. Each volume is backed by S3-compatible object storage and mounted into sandboxes as a read-write directory. Multiple sandboxes can mount the same volume simultaneously, allowing data to be shared across sandboxes and persist independently of the sandbox lifecycle.
### Container registry
Container registries serve as the source for sandbox base images. When creating a [snapshot](https://www.daytona.io/docs/en/snapshots.md), the snapshot builder pulls the specified image from an external registry, and pushes it to the internal snapshot registry for use by runners. For Dockerfile-based snapshots, parent images referenced in `FROM` directives are also pulled from the configured source registries during the build. Daytona supports any OCI-compatible registry:
- [Docker Hub](https://www.daytona.io/docs/en/snapshots.md#docker-hub)
- [Google Artifact Registry](https://www.daytona.io/docs/en/snapshots.md#google-artifact-registry)
- [GitHub Container Registry (GHCR)](https://www.daytona.io/docs/en/snapshots.md#github-container-registry)
- [Private registries](https://www.daytona.io/docs/en/snapshots.md#using-images-from-private-registries): any registry that implements the OCI distribution specification
# Getting Started
This section introduces core concepts, common workflows, and next steps for using Daytona.
## Dashboard
[Daytona Dashboard ↗](https://app.daytona.io/) is a visual user interface where you can manage sandboxes, access API keys, view usage, and more.
It serves as the primary point of control for managing your Daytona resources.
## SDKs
Daytona provides [Python](https://www.daytona.io/docs/en/python-sdk.md), [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), and [Java](https://www.daytona.io/docs/en/java-sdk.md) SDKs to programmatically interact with sandboxes. They support sandbox lifecycle management, code execution, resource access, and more.
## CLI
Daytona provides command-line access to core features for interacting with Daytona Sandboxes, including managing their lifecycle, snapshots, and more. To interact with Daytona Sandboxes from the command line, install the Daytona CLI:
```bash
brew install daytonaio/cli/daytona
```
Trust the tap once so routine `brew upgrade` keeps the Daytona CLI up to date. Recent Homebrew versions require third-party taps to be explicitly trusted; without it, a bare `brew upgrade` skips the Daytona tap and the CLI goes stale:
```bash
brew trust daytonaio/cli
```
To upgrade the Daytona CLI to the latest version:
```bash
brew upgrade daytonaio/cli/daytona
```
Alternatively, install directly without Homebrew:
For Apple Silicon (`arm64`):
```bash
sudo curl -fL https://github.com/daytonaio/daytona/releases/latest/download/daytona-darwin-arm64 -o /usr/local/bin/daytona && sudo chmod +x /usr/local/bin/daytona
```
For Intel (`amd64`):
```bash
sudo curl -fL https://github.com/daytonaio/daytona/releases/latest/download/daytona-darwin-amd64 -o /usr/local/bin/daytona && sudo chmod +x /usr/local/bin/daytona
```
Choose the command for your Linux architecture. Both commands download the latest binary from GitHub releases and install it to `/usr/local/bin`, overwriting any existing version.
For `amd64` (`x86_64`):
```bash
sudo curl -fL https://github.com/daytonaio/daytona/releases/latest/download/daytona-linux-amd64 -o /usr/local/bin/daytona && sudo chmod +x /usr/local/bin/daytona
```
For `arm64` (`aarch64`):
```bash
sudo curl -fL https://github.com/daytonaio/daytona/releases/latest/download/daytona-linux-arm64 -o /usr/local/bin/daytona && sudo chmod +x /usr/local/bin/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
After installing the Daytona CLI, use the `daytona` command to interact with Daytona sandboxes from the command line.
To view all available commands and flags, see the [CLI reference](https://www.daytona.io/docs/en/tools/cli.md).
## API
Daytona provides a RESTful API for interacting with Daytona Sandboxes, including managing their lifecycle, snapshots, and more.
It serves as a flexible and powerful way to interact with Daytona from your own applications.
To interact with Daytona Sandboxes from the API, see the [API reference](https://www.daytona.io/docs/en/tools/api.md).
## MCP server
Daytona provides a Model Context Protocol (MCP) server that enables AI agents to interact with Daytona Sandboxes programmatically. The MCP server integrates with popular AI agents including Claude, Cursor, and Windsurf.
To set up the MCP server with your AI agent:
```bash
daytona mcp init [claude/cursor/windsurf]
```
For more information, see the [MCP server documentation](https://www.daytona.io/docs/en/mcp.md).
## Multiple runtime support
The [TypeScript SDK](https://www.daytona.io/docs/en/typescript-sdk.md) ships as a dual ESM/CJS package and works out of the box in **Node.js**, **Bun**, **Next.js**, **Nuxt.js**, **Remix**, **Vite SSR**, **AWS Lambda**, and **Azure Functions** without any extra configuration.
For **Cloudflare Workers**, set the Node.js compatibility flag in your `wrangler.toml`:
```toml
compatibility_flags = ["nodejs_compat"]
```
For **Deno**, install with `deno add npm:@daytona/sdk` or import directly with the `npm:` specifier:
```typescript
import { Daytona, Image } from 'npm:@daytona/sdk'
```
For **browser apps with Vite** (or any browser bundler), install [`vite-plugin-node-polyfills`](https://www.npmjs.com/package/vite-plugin-node-polyfills) and add it to your `vite.config.ts`:
```typescript
import { defineConfig } from 'vite'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
plugins: [nodePolyfills({ globals: { Buffer: true, process: true, global: true } })],
})
```
The SDK uses Node's `Buffer` for binary data (downloaded files, multipart bodies). Browsers don't ship `Buffer`, so the polyfill provides it. Without it, basic operations like `Image.base()` and `daytona.list()` still work, but methods that handle binary payloads (`fs.downloadFile`, `fs.downloadFiles`) will throw.
Some runtimes don't expose the full set of Node.js APIs (browsers and edge runtimes have no filesystem, no `crypto`, etc.). Methods that depend on those APIs throw a clear runtime error instead of silently producing wrong results.
## Guides
Daytona provides a comprehensive set of [guides](https://www.daytona.io/docs/en/guides.md) to help you get started. The guides cover a wide range of topics, from basic usage to advanced topics, and showcase various types of integrations between Daytona and other tools.
## Examples
Daytona provides quick examples for common sandbox operations and best practices.
The examples are based on the Daytona [Python](https://www.daytona.io/docs/en/python-sdk.md), [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Java](https://www.daytona.io/docs/en/java-sdk.md) **SDKs**, [CLI](https://www.daytona.io/docs/en/tools/cli.md), and [API](https://www.daytona.io/docs/en/tools/api.md) references. More examples are available in our [GitHub repository](https://github.com/daytonaio/daytona/tree/main/examples).
### Create a sandbox
Create a [sandbox](https://www.daytona.io/docs/en/sandboxes.md) with default settings.
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
print(f"Sandbox ID: {sandbox.id}")
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create();
console.log(`Sandbox ID: ${sandbox.id}`);
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Sandbox ID: %s\n", sandbox.ID)
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
puts "Sandbox ID: #{sandbox.id}"
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
System.out.println("Sandbox ID: " + sandbox.getId());
}
}
}
```
```shell
daytona create
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{}'
```
### Create and run code in a sandbox
Create a [sandbox](https://www.daytona.io/docs/en/sandboxes.md) and run code securely in it.
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
response = sandbox.process.exec("echo 'Hello, World!'")
print(response.result)
sandbox.delete()
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create();
const response = await sandbox.process.executeCommand('echo "Hello, World!"');
console.log(response.result);
await sandbox.delete();
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(context.Background(), "echo 'Hello, World!'")
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
sandbox.Delete(context.Background())
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
response = sandbox.process.exec(command: "echo 'Hello, World!'")
puts response.result
daytona.delete(sandbox)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.ExecuteResponse;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
ExecuteResponse response = sandbox.process.executeCommand("echo 'Hello, World!'");
System.out.println(response.getResult());
sandbox.delete();
}
}
}
```
```shell
daytona create --name my-sandbox
daytona exec my-sandbox -- echo 'Hello, World!'
daytona delete my-sandbox
```
```bash
# Create a sandbox
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{}'
# Execute a command in the sandbox
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"command": "echo '\''Hello, World!'\''"
}'
# Delete the sandbox
curl 'https://app.daytona.io/api/sandbox/{sandboxId}' \
--request DELETE \
--header 'Authorization: Bearer '
```
### Create a sandbox with custom resources
Create a sandbox with [custom resources](https://www.daytona.io/docs/en/sandboxes.md#resources) (CPU, memory, disk).
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image, Resources
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromImageParams(
image=Image.debian_slim("3.12"),
resources=Resources(cpu=2, memory=4, disk=8)
)
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create({
image: Image.debianSlim('3.12'),
resources: { cpu: 2, memory: 4, disk: 8 }
});
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(context.Background(), types.ImageParams{
Image: daytona.DebianSlim(nil),
Resources: &types.Resources{
CPU: 2,
Memory: 4,
Disk: 8,
},
})
if err != nil {
log.Fatal(err)
}
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.debian_slim('3.12'),
resources: Daytona::Resources.new(cpu: 2, memory: 4, disk: 8)
)
)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
import io.daytona.sdk.model.Resources;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage(Image.debianSlim("3.12"));
Resources resources = new Resources();
resources.setCpu(2);
resources.setMemory(4);
resources.setDisk(8);
params.setResources(resources);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```shell
daytona create --class small
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"cpu": 2,
"memory": 4,
"disk": 8
}'
```
### Create an ephemeral sandbox
Create an [ephemeral sandbox](https://www.daytona.io/docs/en/sandboxes.md#ephemeral-sandboxes) that is automatically deleted when stopped.
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(ephemeral=True, auto_stop_interval=5)
)
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create({
ephemeral: true,
autoStopInterval: 5
});
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
autoStop := 5
sandbox, err := client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Ephemeral: true,
AutoStopInterval: &autoStop,
},
})
if err != nil {
log.Fatal(err)
}
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(ephemeral: true, auto_stop_interval: 5)
)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setAutoDeleteInterval(0); // same effect as ephemeral: true
params.setAutoStopInterval(5);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```shell
daytona create --auto-stop 5 --auto-delete 0
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"autoStopInterval": 5,
"autoDeleteInterval": 0
}'
```
### Create a sandbox from a snapshot
Create a sandbox from a pre-built [snapshot](https://www.daytona.io/docs/en/snapshots.md) for faster sandbox creation with pre-installed dependencies.
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
language="python"
)
)
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
language: 'typescript'
});
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(context.Background(), types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
},
})
if err != nil {
log.Fatal(err)
}
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
language: Daytona::CodeLanguage::PYTHON
)
)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
params.setLanguage("python");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```shell
daytona create --snapshot my-snapshot-name
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"snapshot": "my-snapshot-name"
}'
```
### Create a sandbox with a declarative image
Create a sandbox with a [declarative image](https://www.daytona.io/docs/en/declarative-builder.md) that defines dependencies programmatically.
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image
daytona = Daytona()
image = (
Image.debian_slim("3.12")
.pip_install(["requests", "pandas", "numpy"])
.workdir("/home/daytona")
)
sandbox = daytona.create(
CreateSandboxFromImageParams(image=image),
on_snapshot_create_logs=print
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk';
const daytona = new Daytona();
const image = Image.debianSlim('3.12')
.pipInstall(['requests', 'pandas', 'numpy'])
.workdir('/home/daytona');
const sandbox = await daytona.create(
{ image },
{ onSnapshotCreateLogs: console.log }
);
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.DebianSlim(nil).
PipInstall([]string{"requests", "pandas", "numpy"}).
Workdir("/home/daytona")
sandbox, err := client.Create(context.Background(), types.ImageParams{
Image: image,
})
if err != nil {
log.Fatal(err)
}
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.debian_slim('3.12')
.pip_install(['requests', 'pandas', 'numpy'])
.workdir('/home/daytona')
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(image: image),
on_snapshot_create_logs: proc { |chunk| puts chunk }
)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.debianSlim("3.12")
.pipInstall("requests", "pandas", "numpy")
.workdir("/home/daytona");
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage(image);
Sandbox sandbox = daytona.create(params, 60, System.out::println);
}
}
}
```
```shell
daytona create --dockerfile ./Dockerfile
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"buildInfo": {
"dockerfileContent": "FROM python:3.12-slim\nRUN pip install requests pandas numpy\nWORKDIR /home/daytona"
}
}'
```
### Create a sandbox with volumes
Create a sandbox with a [volume](https://www.daytona.io/docs/en/volumes.md) mounted to share data across sandboxes.
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("my-volume", create=True)
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
volumes=[VolumeMount(volume_id=volume.id, mount_path="/home/daytona/data")]
)
)
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const volume = await daytona.volume.get('my-volume', true);
const sandbox = await daytona.create({
volumes: [{ volumeId: volume.id, mountPath: '/home/daytona/data' }]
});
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Get(context.Background(), "my-volume")
if err != nil {
volume, err = client.Volume.Create(context.Background(), "my-volume")
if err != nil {
log.Fatal(err)
}
}
sandbox, err := client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Volumes: []types.VolumeMount{{
VolumeID: volume.ID,
MountPath: "/home/daytona/data",
}},
},
})
if err != nil {
log.Fatal(err)
}
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.get('my-volume', create: true)
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id,
mount_path: '/home/daytona/data'
)]
)
)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.exception.DaytonaNotFoundException;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.Volume;
import io.daytona.sdk.model.VolumeMount;
import java.util.Collections;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume;
try {
volume = daytona.volume().getByName("my-volume");
} catch (DaytonaNotFoundException e) {
volume = daytona.volume().create("my-volume");
}
VolumeMount mount = new VolumeMount();
mount.setVolumeId(volume.getId());
mount.setMountPath("/home/daytona/data");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setVolumes(Collections.singletonList(mount));
Sandbox sandbox = daytona.create(params);
}
}
}
```
```shell
daytona volume create my-volume
daytona create --volume my-volume:/home/daytona/data
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/data"
}
]
}'
```
### Create a sandbox with a Git repository cloned
Create a sandbox with a [Git repository](https://www.daytona.io/docs/en/typescript-sdk/git.md) cloned to manage version control.
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
sandbox.git.clone("https://github.com/daytonaio/daytona.git", "/home/daytona/daytona")
status = sandbox.git.status("/home/daytona/daytona")
print(f"Branch: {status.current_branch}")
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create();
await sandbox.git.clone('https://github.com/daytonaio/daytona.git', '/home/daytona/daytona');
const status = await sandbox.git.status('/home/daytona/daytona');
console.log(`Branch: ${status.currentBranch}`);
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(context.Background(), nil)
if err != nil {
log.Fatal(err)
}
sandbox.Git.Clone(context.Background(), "https://github.com/daytonaio/daytona.git", "/home/daytona/daytona")
status, err := sandbox.Git.Status(context.Background(), "/home/daytona/daytona")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Branch: %s\n", status.CurrentBranch)
}
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
sandbox.git.clone(url: "https://github.com/daytonaio/daytona.git", path: "/home/daytona/daytona")
status = sandbox.git.status("/home/daytona/daytona")
puts "Branch: #{status.current_branch}"
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.GitStatus;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
sandbox.git.clone("https://github.com/daytonaio/daytona.git", "/home/daytona/daytona");
GitStatus status = sandbox.git.status("/home/daytona/daytona");
System.out.println("Branch: " + status.getCurrentBranch());
}
}
}
```
```bash
# Create a sandbox
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{}'
# Clone a Git repository in the sandbox
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/clone' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://github.com/daytonaio/daytona.git",
"path": "/home/daytona/daytona"
}'
# Get repository status
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/status?path=/home/daytona/daytona' \
--header 'Authorization: Bearer '
```
# Sandboxes
Daytona provides **full composable computers** — **sandboxes** — for AI agents. Sandboxes are isolated runtime environments you can manage programmatically to run code. Each sandbox runs in isolation, giving it a dedicated kernel, filesystem, network stack, and allocated vCPU, RAM, and disk. Agents get access to a full composable computer where they can install packages, run servers, compile code, and manage processes.
Sandboxes have **1 vCPU**, **1GB RAM**, and **3GiB disk** by default. Organizations get a maximum sandbox resource limit of **4 vCPUs**, **8GB RAM**, and **10GB disk**.
Sandboxes can use [snapshots](https://www.daytona.io/docs/en/snapshots.md) to capture a fully configured environment (base operating system, installed packages, dependencies and configuration) to create new sandboxes.
Each sandbox has its own network stack with per-sandbox firewall rules. By default, sandboxes follow standard network policies, but you can restrict egress to a specific set of allowed destinations or block all outbound traffic entirely.
- **Sandbox SDKs**: [TypeScript](https://www.daytona.io/docs/en/typescript-sdk/sandbox.md), [Python](https://www.daytona.io/docs/en/python-sdk/sync/sandbox.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk/sandbox.md), [Go](https://www.daytona.io/docs/en/go-sdk/daytona.md#type-sandbox), [Java](https://www.daytona.io/docs/en/java-sdk/sandbox.md)
- **Sandbox API**: [RESTful API](https://www.daytona.io/docs/en/tools/api.md#daytona/tag/sandbox) ([OpenAPI spec](https://www.daytona.io/docs/openapi.json)), [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) ([OpenAPI spec](https://www.daytona.io/docs/toolbox-openapi.json))
- **Sandbox CLI**: [Mac/Linux/Windows](https://www.daytona.io/docs/en/tools/cli.md)
## Create Sandboxes
Daytona provides methods to create sandboxes.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Click **Create** to create a sandbox
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
_, _ = client.Create(ctx, nil)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
}
}
}
```
```bash
daytona create [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
```
### GPU Sandboxes
Daytona provides methods to create GPU sandboxes.
Daytona supports NVIDIA GPU devices for creating GPU sandboxes. Use GPU sandboxes for workloads such as model inference, fine-tuning, and CUDA-accelerated compute.
- **NVIDIA H100**
- **NVIDIA RTX Pro 6000**
Daytona provides a pre-built `daytona-gpu` snapshot for creating GPU sandboxes. Each GPU sandbox is ephemeral and supports up to **16 vCPUs**, **192GB RAM**, and **512GB disk**.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a GPU snapshot (**`daytona-gpu`**)
4. Select **`us-east-1`** region
5. Click **Create** to create a GPU sandbox
```python
from daytona import Daytona, DaytonaConfig, CreateSandboxFromSnapshotParams
daytona = Daytona(DaytonaConfig(target="us-east-1"))
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytona-gpu",
auto_delete_interval=0
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona({
target: "us-east-1",
});
const sandbox = await daytona.create({
snapshot: "daytona-gpu",
ephemeral: true,
});
```
```ruby
require 'daytona'
config = Daytona::Config.new(
target: "us-east-1"
)
daytona = Daytona::Daytona.new(config)
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-gpu',
ephemeral: true
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us-east-1",
})
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-gpu",
SandboxBaseParams: types.SandboxBaseParams{
Ephemeral: true,
},
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us-east-1")
.build();
try (Daytona daytona = new Daytona(config)) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-gpu");
params.setAutoDeleteInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-gpu --target us-east-1 --auto-delete 0
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"target": "us-east-1",
"snapshot": "daytona-gpu",
"autoDeleteInterval": 0
}'
```
To create a GPU sandbox with custom GPU count and types:
1. Create a sandbox from an **`image`**
2. Set the **`auto-delete interval`** to **`0`** (ephemeral)
3. Set the **`GPU`** count to the number of GPUs you want
4. Specify the **`GPU type`**(s): **`H100`** **`RTX-PRO-6000`**
The GPU type field accepts a single value or an ordered list of preferred types.
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image, Resources, GpuType, DaytonaConfig
daytona = Daytona(DaytonaConfig(target="us-east-1"))
sandbox = daytona.create(
CreateSandboxFromImageParams(
image=Image.debian_slim("3.12"),
auto_delete_interval=0,
resources=Resources(
gpu=1,
gpu_type=[GpuType.H100, GpuType.RTX_PRO_6000],
),
)
)
```
```typescript
import { Daytona, GpuType, Image } from "@daytona/sdk";
const daytona = new Daytona({
target: "us-east-1",
});
const sandbox = await daytona.create({
image: Image.debianSlim("3.12"),
autoDeleteInterval: 0,
resources: {
gpu: 1,
gpuType: [GpuType.H100, GpuType.RTX_PRO_6000],
},
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new(
Daytona::Config.new(
target: "us-east-1"
)
)
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.debian_slim('3.12'),
auto_delete_interval: 0,
resources: Daytona::Resources.new(
gpu: 1,
gpu_type: [Daytona::GpuType::H100, Daytona::GpuType::RTX_PRO_6000]
)
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us-east-1",
})
ctx := context.Background()
autoDelete := 0
_, _ = client.Create(ctx, types.ImageParams{
Image: "python:3.12",
SandboxBaseParams: types.SandboxBaseParams{
AutoDeleteInterval: &autoDelete,
},
Resources: &types.Resources{
GPU: 1,
GpuType: []types.GpuType{types.GpuTypeH100, types.GpuTypeRtxPro6000},
},
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
import io.daytona.sdk.model.Resources;
import io.daytona.api.client.model.GpuType;
import java.util.List;
final class CreateGpuSandbox {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us-east-1")
.build();
try (Daytona daytona = new Daytona(config)) {
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage("python:3.12");
params.setAutoDeleteInterval(0);
Resources resources = new Resources();
resources.setGpu(1);
resources.setGpuType(List.of(GpuType.H100, GpuType.RTX_PRO_6000));
params.setResources(resources);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"target": "us-east-1",
"image": "python:3.12",
"autoDeleteInterval": 0,
"gpu": 1,
"gpuType": ["H100", "RTX-PRO-6000"]
}'
```
### Windows Sandboxes
Daytona provides methods to create Windows sandboxes.
Windows sandboxes are Windows OS runtime environments used to run Windows applications. Use Windows sandboxes to run Windows-specific tools and workflows on a consistent Windows baseline.
Daytona provides a pre-built `windows` snapshot for creating Windows sandboxes. The snapshot uses **2 vCPU**, **8GiB** memory, and **30GiB** disk.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a Windows snapshot (**`windows`**)
4. Select **`us`** region
5. Click **Create** to create a Windows sandbox
```python
from daytona import Daytona, DaytonaConfig, CreateSandboxFromSnapshotParams
daytona = Daytona(DaytonaConfig(target="us"))
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="windows",
)
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona({
target: "us",
});
const sandbox = await daytona.create({
snapshot: "windows",
});
```
```ruby
require 'daytona'
config = Daytona::Config.new(
target: "us"
)
daytona = Daytona::Daytona.new(config)
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'windows'
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us",
})
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "windows",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us")
.build();
try (Daytona daytona = new Daytona(config)) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("windows");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot windows --target us
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"target": "us",
"snapshot": "windows"
}'
```
### Android Sandboxes
**Available in the `android` region. Contact [support@daytona.io](mailto:support@daytona.io) to request access.**
Daytona provides methods to create Android sandboxes.
Android sandboxes are Android emulator runtime environments used to run Android applications. Use Android sandboxes to run Android-specific tools and workflows on a consistent Android baseline.
An Android setup pairs a standard Linux sandbox with one or more emulator devices:
- **Base sandbox**: a standard Linux sandbox where your code runs (git, builds, POSIX tooling)
- **Device sandbox(es)**: one or more Android emulator devices, attached as [linked sandboxes](#linked-sandboxes)
Daytona provides pre-built Android snapshots for creating Android sandboxes. The snapshots use **4 vCPU**, **8GiB** memory, and **30GiB** disk.
- **`android-12`**, **`android-13`**, **`android-14`**, **`android-15`**, **`android-16`**
```python
from daytona import (
CreateSandboxFromImageParams,
CreateSandboxFromSnapshotParams,
Daytona,
DaytonaConfig,
Image,
)
daytona = Daytona(DaytonaConfig(target="android"))
# Base sandbox: a Linux machine with adb installed
base = daytona.create(
CreateSandboxFromImageParams(
image=Image.base("ubuntu:24.04").run_commands(
"apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*",
"curl -fsSL -o /tmp/pt.zip https://dl.google.com/android/repository/platform-tools-latest-linux.zip && unzip -q /tmp/pt.zip -d /opt && rm /tmp/pt.zip",
"ln -s /opt/platform-tools/adb /usr/local/bin/adb",
),
)
)
# Device sandbox: an Android 16 emulator linked to the base
device = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="android-16",
name="android-emulator",
linked_sandbox=base.id,
ephemeral=True,
),
timeout=600,
)
# Drive the device from the base over the link network
base.process.exec(f"adb connect {device.name}:5555")
response = base.process.exec(
f"adb -s {device.name}:5555 shell getprop ro.build.version.release"
)
```
```typescript
import { Daytona, Image } from "@daytona/sdk";
const daytona = new Daytona({ target: "android" });
// Base sandbox: a Linux machine with adb installed
const base = await daytona.create({
image: Image.base("ubuntu:24.04").runCommands(
"apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*",
"curl -fsSL -o /tmp/pt.zip https://dl.google.com/android/repository/platform-tools-latest-linux.zip && unzip -q /tmp/pt.zip -d /opt && rm /tmp/pt.zip",
"ln -s /opt/platform-tools/adb /usr/local/bin/adb",
),
});
// Device sandbox: an Android 16 emulator linked to the base
const device = await daytona.create(
{
snapshot: "android-16",
name: "android-emulator",
linkedSandbox: base.id,
ephemeral: true,
},
{ timeout: 600 },
);
// Drive the device from the base over the link network
await base.process.executeCommand(`adb connect ${device.name}:5555`);
const response = await base.process.executeCommand(
`adb -s ${device.name}:5555 shell getprop ro.build.version.release`,
);
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new(Daytona::Config.new(target: 'android'))
# Base sandbox: a Linux machine with adb installed
base = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.base('ubuntu:24.04').run_commands(
'apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*',
'curl -fsSL -o /tmp/pt.zip https://dl.google.com/android/repository/platform-tools-latest-linux.zip && unzip -q /tmp/pt.zip -d /opt && rm /tmp/pt.zip',
'ln -s /opt/platform-tools/adb /usr/local/bin/adb'
)
)
)
# Device sandbox: an Android 16 emulator linked to the base
device = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'android-16',
name: 'android-emulator',
linked_sandbox: base.id,
ephemeral: true,
timeout: 600
)
)
# Drive the device from the base over the link network,
# addressing it by the name set at creation
base.process.exec(command: 'adb connect android-emulator:5555')
response = base.process.exec(
command: 'adb -s android-emulator:5555 shell getprop ro.build.version.release'
)
```
```go
package main
import (
"context"
"fmt"
"time"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/options"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{Target: "android"})
ctx := context.Background()
// Base sandbox: a Linux machine with adb installed
base, _ := client.Create(ctx, types.ImageParams{
Image: daytona.Base("ubuntu:24.04").
Run("apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*").
Run("curl -fsSL -o /tmp/pt.zip https://dl.google.com/android/repository/platform-tools-latest-linux.zip && unzip -q /tmp/pt.zip -d /opt && rm /tmp/pt.zip").
Run("ln -s /opt/platform-tools/adb /usr/local/bin/adb"),
})
// Device sandbox: an Android 16 emulator linked to the base
device, _ := client.Create(ctx, types.SnapshotParams{
Snapshot: "android-16",
SandboxBaseParams: types.SandboxBaseParams{
Name: "android-emulator",
LinkedSandbox: base.ID,
Ephemeral: true,
},
}, options.WithTimeout(10*time.Minute))
// Drive the device from the base over the link network
_, _ = base.Process.ExecuteCommand(ctx, fmt.Sprintf("adb connect %s:5555", device.Name))
response, _ := base.Process.ExecuteCommand(ctx,
fmt.Sprintf("adb -s %s:5555 shell getprop ro.build.version.release", device.Name))
fmt.Println(response.Result)
}
```
```bash
# Create the base sandbox (a Linux machine with adb installed)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"target": "android",
"buildInfo": {
"dockerfileContent": "FROM ubuntu:24.04\nRUN apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && rm -rf /var/lib/apt/lists/*\nRUN curl -fsSL -o /tmp/pt.zip https://dl.google.com/android/repository/platform-tools-latest-linux.zip && unzip -q /tmp/pt.zip -d /opt && rm /tmp/pt.zip\nRUN ln -s /opt/platform-tools/adb /usr/local/bin/adb\n"
}
}'
# Create the device sandbox
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"target": "android",
"snapshot": "android-16",
"name": "android-emulator",
"linkedSandbox": "BASE_SANDBOX_ID",
"autoDeleteInterval": 0
}'
```
##### SSH port forwarding
For connecting tools like Android Studio from your machine, tunnel through the base sandbox, not the device. In the following snippet, the device appears as `localhost:5555`:
```bash
ssh -L 5555:LINKED_SANDBOX_ID:5555 SSH_ACCESS_TOKEN@ssh.app.daytona.io
```
##### Customization rules
- **Custom device snapshots**: Available on request. Device images are built by Daytona from a specification you provide. Contact [support@daytona.io](mailto:support@daytona.io) for more information.
### Linked Sandboxes
Daytona provides methods to create linked sandboxes.
Linked sandboxes are attached to an existing parent sandbox at creation time.
Create the parent sandbox first, then create one or more children whose create request references the parent's sandbox ID. This records the relationship on the child sandbox as the linked sandbox ID. Omitting the linked sandbox parameter yields an unlinked sandbox.
- **Lifecycle**
Linked sandboxes are always ephemeral and cannot be persisted or resumed after stop. The [auto-delete interval](#auto-delete-interval) must be exactly `0` on create; this is enforced, not a default. The [auto-stop interval](#auto-stop-interval) sets the idle period in minutes after which the child sandbox stops. Once stopped, linked children are auto-deleted. Deleting the parent deletes all of its linked children (cascade). One parent may have many linked children (1:N).
- **Networking**
Linked sandboxes share an internal link network. Connections work in both directions: the parent can reach each child and each child can reach the parent. Every sandbox on the link network is registered under its sandbox name and ID as DNS aliases, so either works as the host. For example: `telnet LINKED_SANDBOX_ID 5555` from the parent reaches port `5555` on the linked child sandbox.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
parent = daytona.create()
child = daytona.create(
CreateSandboxFromSnapshotParams(
linked_sandbox=parent.id,
ephemeral=True,
)
)
# The link network registers each sandbox under its name as a DNS alias
response = child.process.exec(f"curl http://{parent.name}:3000/")
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const parent = await daytona.create();
const child = await daytona.create({
linkedSandbox: parent.id,
ephemeral: true,
});
// The link network registers each sandbox under its name as a DNS alias
const response = await child.process.executeCommand(
`curl http://${parent.name}:3000/`
);
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
parent = daytona.create
child = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
linked_sandbox: parent.id,
ephemeral: true
)
)
# The link network registers each sandbox under its name and ID as DNS aliases.
# The Ruby SDK does not expose the sandbox name, so address the parent by ID.
response = child.process.exec(command: "curl http://#{parent.id}:3000/")
```
```go
package main
import (
"context"
"fmt"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
parent, _ := client.Create(ctx, types.SnapshotParams{})
child, _ := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
LinkedSandbox: parent.ID,
Ephemeral: true,
},
})
// The link network registers each sandbox under its name as a DNS alias
response, _ := child.Process.ExecuteCommand(ctx, fmt.Sprintf("curl http://%s:3000/", parent.Name))
fmt.Println(response.Result)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox parent = daytona.create();
CreateSandboxFromSnapshotParams childParams = new CreateSandboxFromSnapshotParams();
childParams.setLinkedSandbox(parent.getId());
childParams.setAutoDeleteInterval(0); // linked sandboxes must be ephemeral
Sandbox child = daytona.create(childParams);
// The link network registers each sandbox under its name as a DNS alias
ExecuteResponse response = child.getProcess()
.executeCommand("curl http://" + parent.getName() + ":3000/");
}
}
}
```
```bash
# Create parent sandbox
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
# Create linked child sandbox (replace PARENT_SANDBOX_ID with the id from the first response)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"linkedSandbox": "PARENT_SANDBOX_ID",
"autoDeleteInterval": 0
}'
```
### Ephemeral Sandboxes
Daytona provides methods to create ephemeral sandboxes.
Ephemeral sandboxes are automatically deleted once they are stopped. They are useful for short-lived tasks or testing purposes.
To create an ephemeral sandbox, set the `ephemeral` parameter to `True` when creating a sandbox. Setting [**`autoDeleteInterval: 0`**](#auto-delete-interval) (ephemeral) has the same effect.
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
params = CreateSandboxFromSnapshotParams(
ephemeral=True,
auto_stop_interval=5, # delete after 5 minutes of inactivity
)
sandbox = daytona.create(params)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const sandbox = await daytona.create({
ephemeral: true,
autoStopInterval: 5, // delete after 5 minutes of inactivity
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
params = Daytona::CreateSandboxFromSnapshotParams.new(
ephemeral: true,
auto_stop_interval: 5 # delete after 5 minutes of inactivity
)
sandbox = daytona.create(params)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
autoStopInterval := 5 // delete after 5 minutes of inactivity
params := types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Ephemeral: true,
AutoStopInterval: &autoStopInterval,
},
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setAutoDeleteInterval(0); // same effect as ephemeral: true
params.setAutoStopInterval(5); // delete after 5 minutes of inactivity
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
# --auto-delete 0 has the same effect as ephemeral: true
daytona create --auto-delete 0 --auto-stop 5
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"autoDeleteInterval": 0,
"autoStopInterval": 5
}'
```
### Resources
Sandboxes have **1 vCPU**, **1GB RAM**, and **3GiB disk** by default. Organizations get a maximum sandbox resource limit of **4 vCPUs**, **8GB RAM**, and **10GB disk**.
| **Resource** | **Unit** | **Default** | **Minimum** | **Maximum** |
| ------------ | -------- | ----------- | ----------- | ----------- |
| CPU | vCPU | **`1`** | **`1`** | **`4`** |
| Memory | GiB | **`1`** | **`1`** | **`8`** |
| Disk | GiB | **`3`** | **`1`** | **`10`** |
##### Pre-built snapshots
Daytona provides [pre-built snapshots](https://www.daytona.io/docs/en/snapshots.md#default-snapshots) with fixed resource sizes for creating sandboxes.
| **Snapshot** | **vCPU** | **Memory** | **Storage** | **GPU** |
| -------------------- | -------- | ---------- | ----------- | ------- |
| **`daytona-small`** | 1 | 1GiB | 3GiB | |
| **`daytona-medium`** | 2 | 4GiB | 8GiB | |
| **`daytona-large`** | 4 | 8GiB | 10GiB | |
| **`daytona-gpu`** | 1 | 1GiB | 1GiB | 1 |
| **`windows`** | 2 | 8GiB | 30GiB | |
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytona-medium",
)
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const sandbox = await daytona.create({
snapshot: "daytona-medium",
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-medium'
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-medium",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-medium");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-medium
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "daytona-medium"
}'
```
##### Custom resources
Daytona provides methods to create sandboxes with custom resources.
Use the `Resources` class to set custom sandbox resources. All resource parameters are optional and must be integers. If not specified, Daytona will use the default values. Maximum values are per-sandbox limits set at the organization level.
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image, Resources
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromImageParams(
image=Image.debian_slim("3.12"),
resources=Resources(cpu=2, memory=4, disk=8),
)
)
```
```typescript
import { Daytona, Image } from "@daytona/sdk";
const daytona = new Daytona();
const sandbox = await daytona.create({
image: Image.debianSlim("3.12"),
resources: { cpu: 2, memory: 4, disk: 8 },
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.debian_slim('3.12'),
resources: Daytona::Resources.new(
cpu: 2,
memory: 4,
disk: 8
)
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
_, _ = client.Create(ctx, types.ImageParams{
Image: "python:3.12",
Resources: &types.Resources{
CPU: 2,
Memory: 4,
Disk: 8,
},
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
import io.daytona.sdk.model.Resources;
final class CreateSandboxResources {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage("python:3.12");
Resources resources = new Resources();
resources.setCpu(2);
resources.setMemory(4);
resources.setDisk(8);
params.setResources(resources);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --cpu 2 --memory 4 --disk 8
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"image": "python:3.12",
"cpu": 2,
"memory": 4,
"disk": 8
}'
```
## Start Sandboxes
Daytona provides methods to start sandboxes.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the start icon (**▶**) next to the sandbox you want to start
```python
sandbox.start()
```
```typescript
await sandbox.start()
```
```ruby
sandbox.start
```
```go
sandbox.Start(ctx)
```
```java
sandbox.start();
```
```bash
daytona start [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/start' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Get Sandbox
Daytona provides methods to get a sandbox by ID or name.
```python
sandbox = daytona.get("my-sandbox-id-or-name")
```
```typescript
const sandbox = await daytona.get("my-sandbox-id-or-name");
```
```ruby
sandbox = daytona.get('my-sandbox-id-or-name')
```
```go
sandbox, err := client.Get(ctx, "my-sandbox-id-or-name")
```
```java
Sandbox sandbox = daytona.get("my-sandbox-id-or-name");
```
```bash
daytona info [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## List Sandboxes
Daytona provides methods to list sandboxes.
```python
for sandbox in daytona.list():
print(sandbox.id)
```
```typescript
for await (const sandbox of daytona.list()) {
console.log(sandbox.id)
}
```
```ruby
daytona.list.each { |sandbox| puts sandbox.id }
```
```go
iter := client.List(ctx, nil)
defer iter.Close()
for iter.Next() {
sandbox := iter.Value()
fmt.Println(sandbox.ID)
}
if err := iter.Err(); err != nil {
log.Fatal(err)
}
```
```java
Iterator> iter = daytona.list();
while (iter.hasNext()) {
Map sandbox = iter.next();
System.out.println(sandbox.get("id"));
}
```
```bash
daytona list [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Stop Sandboxes
Daytona provides methods to stop sandboxes.
Stopped sandboxes maintain filesystem persistence while their memory state is cleared. They incur
only disk usage costs and can be started again when needed. Sandboxes in a stopping or stopped state
will no longer accept requests.
The stopped state should be used when a sandbox is expected to be started again. Otherwise, it is recommended to stop and then archive the sandbox to eliminate disk usage costs.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the stop icon (**⏹**) next to the sandbox you want to stop
```python
sandbox.stop()
```
```typescript
await sandbox.stop()
```
```ruby
sandbox.stop
```
```go
sandbox.Stop(ctx)
```
```java
sandbox.stop();
```
```bash
daytona stop [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/stop' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
If you need a faster shutdown, use force stop (`force=true` / `--force`) to terminate the sandbox immediately. Force stop is ungraceful and should be used when quick termination is more important than process cleanup. Avoid force stop for normal shutdowns where the process should flush buffers, write final state, or run cleanup hooks.
Common use cases for force stop include:
- you need to reduce stop time and can accept immediate termination
- the entrypoint ignores termination signals or hangs during shutdown
## Pause Sandboxes
Daytona provides methods to pause sandboxes.
Pausing a sandbox keeps both filesystem state and memory persistence, so sandboxes can resume from in-memory runtime state. Compared to regular stop behavior, pause is useful for workloads with active in-memory context and state continuity.
Daytona supports pause functionality through VM-based runners. Pause is handled through the existing stop action. This means stop behaves as pause and preserves memory state, while force stop performs a full shutdown without preserving memory state.
## Archive Sandboxes
Daytona provides methods to archive sandboxes.
A sandbox must be stopped before it can be archived. When a sandbox is archived, the entire filesystem state is moved to a cost-effective object storage, making it available for an extended period.
Starting an archived sandbox takes more time than starting a stopped sandbox, depending on its size. It can be started again in the same way as a stopped sandbox.
```python
sandbox.archive()
```
```typescript
await sandbox.archive()
```
```ruby
sandbox.archive
```
```go
sandbox.Archive(ctx)
```
```bash
daytona archive [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/archive' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Recover Sandboxes
Daytona provides methods to recover sandboxes.
```python
sandbox.recover()
```
```typescript
await sandbox.recover()
```
```ruby
sandbox.recover
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/recover' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
When a sandbox enters an error state, it can sometimes be recovered using the `recover` method, depending on the underlying error reason. The `recoverable` flag indicates whether the error state can be resolved through an automated recovery procedure.
Recovery actions are not performed automatically because they address errors that require **further user intervention**, such as freeing up storage space.
```python
# Check if the sandbox is recoverable
if sandbox.recoverable:
sandbox.recover()
```
```typescript
// Check if the sandbox is recoverable
if (sandbox.recoverable) {
await sandbox.recover()
}
```
```ruby
# Check if the sandbox is in an error state before recovering
if sandbox.state == 'error'
sandbox.recover
end
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/recover' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Resize Sandboxes
Daytona provides methods to resize [sandbox resources](#resources) after creation.
On a running sandbox, you can increase CPU and memory without interruption. To decrease CPU or memory, or to increase disk capacity, stop the sandbox first. Disk size can only be increased and cannot be decreased.
Resizing updates the sandbox resource allocation (`cpu`, `memory`, and `disk`) for that sandbox only. CPU and memory control compute capacity for running workloads, while disk controls persistent filesystem capacity. Values must be integers and stay within your organization's per-sandbox resource limits.
```python
# Resize a started sandbox (CPU and memory can be increased)
sandbox.resize(Resources(cpu=2, memory=4))
# Resize a stopped sandbox (CPU and memory can change, disk can only increase)
sandbox.stop()
sandbox.resize(Resources(cpu=4, memory=8, disk=20))
sandbox.start()
```
```typescript
// Resize a started sandbox (CPU and memory can be increased)
await sandbox.resize({ cpu: 2, memory: 4 })
// Resize a stopped sandbox (CPU and memory can change, disk can only increase)
await sandbox.stop()
await sandbox.resize({ cpu: 4, memory: 8, disk: 20 })
await sandbox.start()
```
```ruby
# Resize a started sandbox (CPU and memory can be increased)
sandbox.resize(Daytona::Resources.new(cpu: 2, memory: 4))
# Resize a stopped sandbox (CPU and memory can change, disk can only increase)
sandbox.stop
sandbox.resize(Daytona::Resources.new(cpu: 4, memory: 8, disk: 20))
sandbox.start
```
```go
// Resize a started sandbox (CPU and memory can be increased)
err := sandbox.Resize(ctx, &types.Resources{CPU: 2, Memory: 4})
// Resize a stopped sandbox (CPU and memory can change, disk can only increase)
err = sandbox.Stop(ctx)
err = sandbox.Resize(ctx, &types.Resources{CPU: 4, Memory: 8, Disk: 20})
err = sandbox.Start(ctx)
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/resize' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"cpu": 2,
"memory": 4,
"disk": 20
}'
```
To verify CPU and memory limits inside the sandbox after resizing, read `cgroup` values directly. Tools such as `nproc`, `free`, `top`, `htop`, `/proc/cpuinfo`, and `/proc/meminfo` read host-level values and do not reflect sandbox resource limits.
```bash
cat /sys/fs/cgroup/cpu.max # " " (cores = quota / period)
cat /sys/fs/cgroup/memory.max # bytes
df -h / # disk
```
## Fork Sandboxes
Daytona provides methods to fork sandboxes.
Forking creates a duplicate of your sandbox's filesystem and memory, and copies it into a new sandbox. The new sandbox is fully independent: it can be started, stopped, and deleted without affecting the original. The sandbox must be in started state before forking.
Daytona tracks the parent-child relationship in a fork tree, so you can always trace a fork's lineage back to the sandbox it was created from. You can fork a fork, building out branches as needed. The parent sandbox cannot be deleted while it has active fork children.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the three-dot menu (**⋮**) next to the sandbox you want to fork
3. Select **Fork**
```python
# Fork sandbox through the Sandbox instance
forked = sandbox._experimental_fork(name="my-forked-sandbox")
```
```typescript
// Fork sandbox through the Sandbox instance
const forkedSandbox = await sandbox._experimental_fork({ name: "my-forked-sandbox" });
// Or use the Daytona helper method
const forkedSandbox = await daytona._experimental_fork(sandbox, { name: "my-forked-sandbox" });
```
```ruby
# Fork sandbox through the Sandbox instance
forkedSandbox = sandbox.experimental_fork(name: "my-forked-sandbox")
```
```go
// Fork sandbox through the Sandbox instance
name := "my-forked-sandbox"
forkedSandbox, err := sandbox.ExperimentalFork(ctx, &name)
if err != nil {
return err
}
```
```java
// Fork sandbox through the Sandbox instance
Sandbox forkedSandbox = sandbox.experimentalFork("my-forked-sandbox", 60);
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/fork' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-forked-sandbox"
}'
```
To view the fork tree for a sandbox and all its related sandboxes:
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the three-dot menu (**⋮**) next to a sandbox
3. Select **View Fork Tree**
The fork tree displays each sandbox in the hierarchy along with its current state and creation time, allowing you to trace the lineage of any fork back to its origin.
## Label Sandboxes
Daytona provides methods to set sandbox labels.
Setting labels replaces the full label set for the sandbox. Include all labels you want to keep in the request. If you omit an existing label, it will be removed.
```python
sandbox.set_labels({
"team": "platform",
"env": "staging",
})
```
```typescript
await sandbox.setLabels({
team: "platform",
env: "staging",
});
```
```ruby
sandbox.labels = {
team: 'platform',
env: 'staging'
}
```
```go
err := sandbox.SetLabels(ctx, map[string]string{
"team": "platform",
"env": "staging",
})
```
```java
Map labels = new HashMap<>();
labels.put("team", "platform");
labels.put("env", "staging");
sandbox.setLabels(labels);
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/labels' \
--request PUT \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"labels": {
"team": "platform",
"env": "staging"
}
}'
```
## Create Snapshot from Sandbox
Daytona provides methods to create [snapshots](https://www.daytona.io/docs/en/snapshots.md) from sandboxes.
A snapshot captures an immutable, point-in-time copy of a sandbox's filesystem and memory that you can use as a base to create new sandboxes, effectively templating a known-good environment for reuse. You can think of it as a checkpoint you can restore from whenever you need a clean, identical starting point.
[Windows sandboxes](#windows-sandboxes) use the `includeMemory` parameter to control whether the snapshot also captures the sandbox's memory.
| **Include memory** | **Snapshot contents** | **Required sandbox state** |
| ------------------- | ------------------------- | -------------------------- |
| **`false`** (default) | Filesystem only | Stopped |
| **`true`** | Filesystem and memory | Started |
```python
# Create snapshot from sandbox
sandbox._experimental_create_snapshot("my-sandbox-snapshot")
```
```typescript
// Create snapshot from sandbox
await sandbox._experimental_createSnapshot("my-sandbox-snapshot");
```
```ruby
# Create snapshot from sandbox
sandbox.experimental_create_snapshot(name: "my-sandbox-snapshot")
```
```go
// Create snapshot from sandbox
err := sandbox.ExperimentalCreateSnapshot(ctx, "my-sandbox-snapshot")
if err != nil {
return err
}
```
```java
// Create snapshot from sandbox
sandbox.experimentalCreateSnapshot("my-sandbox-snapshot");
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/snapshot' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-sandbox-snapshot",
"includeMemory": false
}'
```
## Delete Sandboxes
Daytona provides methods to delete sandboxes.
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the **Delete** button next to the sandbox you want to delete.
```python
sandbox.delete()
```
```typescript
await sandbox.delete()
```
```ruby
sandbox.delete
```
```go
err = sandbox.Delete(ctx)
```
```java
sandbox.delete();
```
```bash
daytona delete [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}' \
--request DELETE \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Sandbox lifecycle
A sandbox can have several different states. Each state reflects the status of your sandbox.
- [**Creating**](#create-sandboxes): the sandbox is provisioning and will be ready to use
- [**Starting**](#start-sandboxes): the sandbox is starting and will be ready to use
- [**Started**](#start-sandboxes): the sandbox has started and is ready to use
- [**Stopping**](#stop-sandboxes): the sandbox is stopping and will no longer accept requests
- [**Stopped**](#stop-sandboxes): the sandbox has stopped and is no longer running
- [**Deleting**](#delete-sandboxes): the sandbox is deleting and will be removed
- [**Deleted**](#delete-sandboxes): the sandbox has been deleted and no longer exists
- [**Archiving**](#archive-sandboxes): the sandbox is archiving and its state will be preserved
- [**Archived**](#archive-sandboxes): the sandbox has been archived and its state is preserved
- [**Resizing**](#resize-sandboxes): the sandbox is being resized to a new set of resources
- [**Error**](#recover-sandboxes): the sandbox is in an error state and needs to be recovered
- **Restoring**: the sandbox is being restored from archive and will be ready to use shortly
- **Unknown**: the default sandbox state before it is created
- **Pulling Snapshot**: the sandbox is pulling a [snapshot](https://www.daytona.io/docs/en/snapshots.md) to provide a base environment
- **Building Snapshot**: the sandbox is building a [snapshot](https://www.daytona.io/docs/en/snapshots.md) to provide a base environment
- **Build Pending**: the sandbox build is pending and will start shortly
- **Build Failed**: the sandbox build failed and needs to be retried
The diagram demonstrates the states and possible transitions between them.
## Multiple runtime support
Daytona sandboxes support Python, TypeScript, and JavaScript programming language runtimes for direct code execution inside the sandbox. The `language` parameter controls which programming language runtime is used for the sandbox:
- **`python`**
- **`typescript`**
- **`javascript`**
If omitted, the Daytona SDK will default to `python`. To override this, explicitly set the `language` value when creating the sandbox.
## Automated lifecycle management
Sandboxes can be automatically stopped, archived, and deleted based on user-defined intervals. The intervals act as a TTL (time-to-live) mechanism for the sandbox. You can also refresh the last activity timestamp to explicitly signal activity when lifecycle behavior depends on inactivity intervals.
### Update sandbox last activity
Daytona provides methods to update a sandbox's last activity timestamp.
This updates the sandbox's recorded activity time without changing its runtime state. It is useful when your workflow is driven by external systems or background orchestration that may not reset inactivity tracking.
For example, if you run long-lived automation around a sandbox and want to avoid unintended auto-stop behavior, call this operation periodically to indicate that the sandbox is still actively used.
```python
sandbox.refresh_activity()
```
```typescript
await sandbox.refreshActivity();
```
```ruby
sandbox.refresh_activity
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/last-activity' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Auto-stop interval
Daytona provides methods to set the auto-stop interval.
The auto-stop interval sets the amount of time after which a running sandbox will be automatically stopped. The auto-stop triggers even if there are internal processes running in the sandbox.
The system differentiates between "internal processes" and "active user interaction". Merely having a script or background task running is not sufficient to keep the sandbox alive.
- [What resets the timer](#what-resets-the-timer)
- [What does not reset the timer](#what-does-not-reset-the-timer)
The parameter can either be set to:
- a time interval in minutes
- `0`: disables the auto-stop functionality, allowing the sandbox to run indefinitely
If the parameter is not set, the default interval of `15 minutes` will be used.
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval=0,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval: 0,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval: 0
)
)
```
```go
// Create a sandbox with auto-stop disabled
autoStopInterval := 0
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoStopInterval: &autoStopInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Disables the auto-stop feature - default is 15 minutes
params.setAutoStopInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autostop/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
##### What resets the timer
The inactivity timer resets only for specific external interactions:
- Updates to [sandbox lifecycle states](#sandbox-lifecycle)
- Network requests through [sandbox previews](https://www.daytona.io/docs/en/preview.md)
- Active [SSH connections](https://www.daytona.io/docs/en/ssh-access.md)
- API requests to the [Daytona Toolbox SDK](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox)
##### What does not reset the timer
The following do not reset the timer:
- SDK requests that are not toolbox actions
- Background scripts (e.g., `npm run dev` run as a fire-and-forget command)
- Long-running tasks without external interaction
- Processes that don't involve active monitoring
If you run a long-running task like LLM inference that takes more than 15 minutes to complete without any external interaction, the sandbox may auto-stop mid-process because the process itself doesn't count as "activity", therefore the timer is not reset.
### Auto-archive interval
Daytona provides methods to set the auto-archive interval.
The auto-archive interval sets the amount of time after which a continuously stopped sandbox will be automatically archived. The parameter can either be set to:
- a time interval in minutes
- `0`: the maximum interval of `30 days` will be used
If the parameter is not set, the default interval of `7 days` will be used.
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Auto-archive after a sandbox has been stopped for 1 hour
auto_archive_interval=60,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Auto-archive after a sandbox has been stopped for 1 hour
autoArchiveInterval: 60,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Auto-archive after a sandbox has been stopped for 1 hour
auto_archive_interval: 60
)
)
```
```go
// Create a sandbox with auto-archive after 1 hour
autoArchiveInterval := 60
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoArchiveInterval: &autoArchiveInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Auto-archive after a sandbox has been stopped for 1 hour
params.setAutoArchiveInterval(60);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autoarchive/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Auto-delete interval
Daytona provides methods to set the auto-delete interval.
The auto-delete interval sets the amount of time after which a continuously stopped sandbox will be automatically deleted. By default, sandboxes will never be automatically deleted. The parameter can either be set to:
- a time interval in minutes
- `-1`: disables the auto-delete functionality
- `0`: the sandbox will be deleted immediately after stopping
If the parameter is not set, the sandbox will not be deleted automatically.
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Auto-delete after a sandbox has been stopped for 1 hour
auto_delete_interval=60,
))
# Delete the sandbox immediately after it has been stopped
sandbox.set_auto_delete_interval(0)
# Disable auto-deletion
sandbox.set_auto_delete_interval(-1)
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Auto-delete after a sandbox has been stopped for 1 hour
autoDeleteInterval: 60,
})
// Delete the sandbox immediately after it has been stopped
await sandbox.setAutoDeleteInterval(0)
// Disable auto-deletion
await sandbox.setAutoDeleteInterval(-1)
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Auto-delete after a sandbox has been stopped for 1 hour
auto_delete_interval: 60
)
)
# Delete the sandbox immediately after it has been stopped
sandbox.auto_delete_interval = 0
# Disable auto-deletion
sandbox.auto_delete_interval = -1
```
```go
// Create a sandbox with auto-delete after 1 hour
autoDeleteInterval := 60
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoDeleteInterval: &autoDeleteInterval,
},
}
sandbox, err := client.Create(ctx, params)
// Delete the sandbox immediately after it has been stopped
zeroInterval := 0
err = sandbox.SetAutoDeleteInterval(ctx, &zeroInterval)
// Disable auto-deletion
disableInterval := -1
err = sandbox.SetAutoDeleteInterval(ctx, &disableInterval)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Auto-delete after a sandbox has been stopped for 1 hour
params.setAutoDeleteInterval(60);
Sandbox sandbox = daytona.create(params);
// Delete the sandbox immediately after it has been stopped
sandbox.setAutoDeleteInterval(0);
// Disable auto-deletion
sandbox.setAutoDeleteInterval(-1);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autodelete/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Running indefinitely
Daytona provides methods to run sandboxes indefinitely.
By default, Daytona sandboxes auto-stop after 15 minutes of inactivity. To keep a sandbox running without interruption, set the auto-stop interval to `0` when creating a new sandbox:
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my_awesome_snapshot",
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval=0,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my_awesome_snapshot',
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval: 0,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my_awesome_snapshot',
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval: 0
)
)
```
```go
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval := 0
params := types.SnapshotParams{
Snapshot: "my_awesome_snapshot",
SandboxBaseParams: types.SandboxBaseParams{
AutoStopInterval: &autoStopInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my_awesome_snapshot");
// Disables the auto-stop feature - default is 15 minutes
params.setAutoStopInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
# Environment Configuration
Daytona supports multiple methods to configure your environment, in order of precedence:
1. [Configuration in code](#configuration-in-code)
2. [Environment variables](#environment-variables)
3. [.env file](#env-file)
4. [Default values](#default-values)
## Configuration in code
To configure your environment in code, use the `DaytonaConfig` class. The `DaytonaConfig` class accepts the following parameters:
- `api_key`: Your Daytona [API Key](https://www.daytona.io/docs/api-keys.md)
- `api_url`: URL of your [Daytona API](https://www.daytona.io/docs/en/tools/api.md)
- `target`: Target region to create the Sandboxes on (`us` / `eu`)
```python
from daytona import DaytonaConfig
config = DaytonaConfig(
api_key="YOUR_API_KEY",
api_url="YOUR_API_URL",
target="us"
)
```
```typescript
import { DaytonaConfig } from '@daytona/sdk'
const config: DaytonaConfig = {
apiKey: 'YOUR_API_KEY',
apiUrl: 'YOUR_API_URL',
target: 'us',
}
```
```ruby
require 'daytona'
config = Daytona::Config.new(
api_key: 'YOUR_API_KEY',
api_url: 'YOUR_API_URL',
target: 'us'
)
```
```go
package main
import (
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
config := daytona.Config{
APIKey: "YOUR_API_KEY",
APIURL: "YOUR_API_URL",
Target: "us",
}
client := daytona.NewClient(&config)
_ = client
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
public class App {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey("YOUR_API_KEY")
.apiUrl("YOUR_API_URL")
.target("us")
.build();
try (Daytona daytona = new Daytona(config)) {
// Application code
}
}
}
```
```bash
curl https://app.daytona.io/api/api-keys \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "",
"permissions": [
"write:registries"
],
"expiresAt": ""
}'
```
## Environment variables
Daytona supports environment variables for configuration. The SDK automatically looks for these environment variables:
| Variable | Description | Required |
| --------------------- | ------------------------------------------ | -------- |
| **`DAYTONA_API_KEY`** | Your Daytona API key. | Yes |
| **`DAYTONA_API_URL`** | URL of your Daytona API. | No |
| **`DAYTONA_TARGET`** | Daytona Target to create the sandboxes on. | No |
### Shell
Set environment variables in your shell using the following methods:
```bash
export DAYTONA_API_KEY=your-api-key
export DAYTONA_API_URL=https://your-api-url
export DAYTONA_TARGET=us
```
```bash
$env:DAYTONA_API_KEY="your-api-key"
$env:DAYTONA_API_URL="https://your-api-url"
$env:DAYTONA_TARGET="us"
```
### .env file
Set the environment variables in a `.env` file using the following format:
```bash
DAYTONA_API_KEY=YOUR_API_KEY
DAYTONA_API_URL=https://your_api_url
DAYTONA_TARGET=us
```
## Default values
If no configuration is provided, Daytona will use its built-in default values:
| **Option** | **Value** |
| ---------- | ----------------------------------- |
| API URL | https://app.daytona.io/api |
| Target | Default region for the organization |
# Snapshots
Snapshots are reusable sandbox templates built from [Docker](https://www.docker.com/) or [OCI](https://opencontainers.org/) compatible images. Sandboxes can use snapshots to provide a consistent and reproducible environment for your dependencies, settings, and resources.
A snapshot defines the base operating system, language runtimes, system packages, and project-level setup that should exist when a sandbox starts. Instead of repeating bootstrap steps on every sandbox creation, you capture that setup once as a snapshot and reuse it.
You start with default snapshots for common stacks, or create custom snapshots for your own toolchain and constraints. Custom snapshots are useful when your workflow depends on specific package versions, private dependencies, startup scripts, or filesystem layout.
- **Snapshot SDKs**: [TypeScript](https://www.daytona.io/docs/en/typescript-sdk/snapshot.md), [Python](https://www.daytona.io/docs/en/python-sdk/sync/snapshot.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk/snapshot.md), [Go](https://www.daytona.io/docs/en/go-sdk/daytona.md#type-snapshotservice), [Java](https://www.daytona.io/docs/en/java-sdk/snapshot.md)
- **Snapshot API**: [RESTful API](https://www.daytona.io/docs/en/tools/api.md#daytona/tag/snapshots) ([OpenAPI spec](https://www.daytona.io/docs/openapi.json)), [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) ([OpenAPI spec](https://www.daytona.io/docs/toolbox-openapi.json))
- **Snapshot CLI**: [Mac/Linux/Windows](https://www.daytona.io/docs/en/tools/cli.md)
## Create Snapshots
Daytona provides methods to create snapshots. You can create a snapshot from:
- [GPU snapshots](#gpu-snapshots) (for [GPU sandboxes](https://www.daytona.io/docs/en/sandboxes.md#gpu-sandboxes))
- [public images](#public-images)
- [local images](#local-images)
- [images from private registries](#images-from-private-registries)
- [declarative builder](#declarative-builder)
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click **Create Snapshot**
3. Enter the snapshot **`name`** and **`image`**
- **Snapshot name**: identifier used to reference the snapshot
- **Snapshot image**: base image for the snapshot, must include either a tag or a digest (e.g., **`ubuntu:22.04`**); the `latest`/`lts`/`stable` tags are not supported
4. Click **Create** to create a snapshot
```python
from daytona import Daytona, CreateSnapshotParams
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(name="my-awesome-snapshot", image="python:3.12"),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-awesome-snapshot",
image: "python:3.12",
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'my-awesome-snapshot', image: 'python:3.12')
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-awesome-snapshot",
Image: "python:3.12",
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.model.Snapshot;
final class CreateSnapshot {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Snapshot snapshot = daytona.snapshot().create("my-awesome-snapshot", "python:3.12");
}
}
}
```
```bash
daytona snapshot create my-awesome-snapshot --image python:3.11-slim --cpu 2 --memory 4
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-awesome-snapshot",
"imageName": "python:3.11-slim",
"cpu": 2,
"memory": 4
}'
```
### GPU Snapshots
Daytona provides methods to create GPU snapshots.
GPU snapshots are used to create [GPU sandboxes](https://www.daytona.io/docs/en/sandboxes.md#gpu-sandboxes). Daytona provides a pre-built `daytona-gpu` snapshot for creating GPU sandboxes.
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click **Create Snapshot**
3. Enter the snapshot **`name`** and **`image`**
4. Select **`us-east-1`** region
5. Select the **`Allocate GPU`** checkbox
6. Click **Create** to create a GPU snapshot
```python
from daytona import CreateSnapshotParams, Daytona, DaytonaConfig, Image, Resources
daytona = Daytona(DaytonaConfig(target="us-east-1"))
snapshot = daytona.snapshot.create(
CreateSnapshotParams(
name="my-gpu-snapshot",
image=Image.base("python:3.12"),
resources=Resources(cpu=1, memory=1, disk=1, gpu=1),
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona({ target: "us-east-1" });
const snapshot = await daytona.snapshot.create({
name: "my-gpu-snapshot",
image: "python:3.12",
resources: { cpu: 1, memory: 1, disk: 1, gpu: 1 },
});
```
```ruby
require 'daytona'
config = Daytona::Config.new(
target: "us-east-1"
)
daytona = Daytona::Daytona.new(config)
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-gpu-snapshot',
image: 'python:3.12',
resources: Daytona::Resources.new(cpu: 1, memory: 1, disk: 1, gpu: 1)
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us-east-1",
})
ctx := context.Background()
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-gpu-snapshot",
Image: "python:3.12",
Resources: &types.Resources{
CPU: 1,
Memory: 1,
Disk: 1,
GPU: 1,
},
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.Image;
import io.daytona.sdk.model.Resources;
import io.daytona.sdk.model.Snapshot;
final class CreateGpuSnapshot {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us-east-1")
.build();
try (Daytona daytona = new Daytona(config)) {
Resources resources = new Resources();
resources.setCpu(1);
resources.setMemory(1);
resources.setDisk(1);
resources.setGpu(1);
Snapshot snapshot = daytona.snapshot().create(
"my-gpu-snapshot",
Image.base("python:3.12"),
resources,
null
);
}
}
}
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-gpu-snapshot",
"imageName": "python:3.12",
"regionId": "us-east-1",
"cpu": 1,
"memory": 1,
"disk": 1,
"gpu": 1
}'
```
### Public images
Daytona supports creating snapshots from any publicly accessible image or container registry.
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the **Create Snapshot** button
3. Enter the snapshot **`name`** and **`image`** of any publicly accessible image or container registry
```python
from daytona import Daytona, CreateSnapshotParams
daytona = Daytona()
daytona.snapshot.create(
CreateSnapshotParams(name="my-awesome-snapshot", image="python:3.11-slim"),
on_logs=lambda chunk: print(chunk, end=""),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
await daytona.snapshot.create(
{ name: "my-awesome-snapshot", image: "python:3.11-slim" },
{ onLogs: console.log },
);
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
params = Daytona::CreateSnapshotParams.new(
name: 'my-awesome-snapshot',
image: 'python:3.11-slim'
)
snapshot = daytona.snapshot.create(params) do |chunk|
print chunk
end
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
snapshot, logChan, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-awesome-snapshot",
Image: "python:3.11-slim",
})
_ = snapshot
for range logChan {
}
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.model.Snapshot;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Snapshot snapshot = daytona.snapshot().create("my-awesome-snapshot", "python:3.11-slim");
}
}
}
```
```bash
daytona snapshot create my-awesome-snapshot --image python:3.11-slim
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-awesome-snapshot",
"imageName": "python:3.11-slim"
}'
```
### Local images
Daytona supports creating snapshots from local images or from local Dockerfiles.
To create a snapshot from a local image or from a local Dockerfile, use the [Daytona CLI](https://www.daytona.io/docs/en/tools/cli.md#daytona-snapshot).
Daytona expects the local image to be built for AMD64 architecture. Therefore, the `--platform=linux/amd64` flag is required when building the Docker image if your machine is running on a different architecture.
1. Ensure the image and tag you want to use is available
```bash
docker images
```
2. Create a snapshot and push it to Daytona:
```bash
daytona snapshot push custom-alpine:3.21 --name alpine-minimal
```
Alternatively, use the `--dockerfile` flag under `create` to pass the path to the Dockerfile you want to use and Daytona will build the snapshot for you. The `COPY`/`ADD` commands will be automatically parsed and added to the context. To manually add files to the context, use the `--context` flag.
```bash
daytona snapshot create my-awesome-snapshot --dockerfile ./Dockerfile
```
### Images from private registries
Daytona supports creating snapshots from images from [Docker Hub](#docker-hub), [Google Artifact Registry](#google-artifact-registry), [GitHub Container Registry](#github-container-registry-ghcr), [Amazon ECR](#amazon-elastic-container-registry-ecr) or other private container registries.
1. Navigate to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries)
2. Click **Add Registry** and select your provider
3. Fill in the visible fields
4. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
5. Click **Create Snapshot**
6. Enter the snapshot **`name`** and the full **`image`** reference, including the registry host and repository (e.g. **`my-registry.com//custom-alpine:3.21`**)
#### Docker Hub
Daytona supports creating snapshots from Docker Hub images.
1. Navigate to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries),
2. Click **Add Registry** and select the **Docker Hub** tab
3. Input the following fields:
- **Username**: your Docker Hub username (the account with access to the image)
- **Personal Access Token**: a [Docker Hub PAT](https://docs.docker.com/security/access-tokens/) — not your account password
- **Registry URL**: auto-filled with **`docker.io`** and not shown in the form
4. Create the snapshot using the full image reference, e.g. **`docker.io//:`**
#### Google Artifact Registry
Daytona supports creating snapshots from images from Google Artifact Registry, authenticated with a [service account key](https://cloud.google.com/iam/docs/keys-create-delete) in JSON format.
1. Navigate to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries),
2. Click **Add Registry** and select the **Google** tab
2. Input the following fields:
- **Registry URL**: the base URL for your region (e.g. **`https://us-central1-docker.pkg.dev`**)
- **Service Account JSON Key**: the contents of your service account key JSON file
- **Google Cloud Project ID**: your GCP project ID
- **Username**: auto-filled with **`_json_key`** (required by Google for service-account auth)
3. Create the snapshot using the full image reference, e.g.
**`us-central1-docker.pkg.dev///:`**
#### GitHub Container Registry
Daytona supports creating snapshots from images from GitHub Container Registry.
1. Navigate to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries),
2. Click **Add Registry** and select the **GitHub** tab
2. Input the following fields:
- **GitHub Username**: the account with access to the image
- **Personal Access Token**: a [GitHub PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with **`read:packages`** scope (and **`write:packages`** / **`delete:packages`** if you'll push or delete)
- **Registry URL**: auto-filled with **`ghcr.io`** and not shown in the form
3. Create the snapshot using the full image reference, e.g.
**`ghcr.io//:`**
#### Amazon Elastic Container Registry
Daytona pulls private ECR images via cross-account IAM role assumption — you create a role in your AWS account that trusts Daytona's broker principal, and Daytona assumes it on every pull to fetch a short-lived ECR token. No long-lived AWS credentials are shared, and no manual token rotation is needed.
You'll need two values:
- **Daytona Broker ARN**: `arn:aws:iam::967657494466:role/DaytonaEcrCredentialBroker` — the IAM principal Daytona uses to assume into your role. Self-hosted: substitute the IAM role your API pods assume (e.g. via IRSA).
- **External ID**: your Daytona organization ID, visible in the dashboard URL (`/dashboard//...`) and on your organization settings page.
##### 1. Create an IAM role in your AWS account
Create an IAM role with the trust and permissions policies below. Replace `` with your organization ID.
Trust policy:
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::967657494466:role/DaytonaEcrCredentialBroker" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": ""
}
}
}]
}
```
Permissions policy (read-only on ECR):
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
}]
}
```
Copy the ARN of the role you just created (e.g. `arn:aws:iam::123456789012:role/daytona-ecr-puller`).
##### 2. Register the registry in Daytona
1. On [Daytona Registries ↗](https://app.daytona.io/dashboard/registries), click **Add Registry** and select the **Amazon ECR** tab.
2. Fill in:
- **Registry URL**: `.dkr.ecr..amazonaws.com`
- **Role ARN**: the role you created in step 1 — Daytona assumes it on every pull
Password is not used for ECR — Daytona resolves credentials server-side by assuming the role you created in step 1, using your organization ID as the AssumeRole `ExternalId`.
##### 3. Create the snapshot
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots).
2. Click **Create Snapshot**.
3. Enter the snapshot name and the full image reference (e.g. `123456789012.dkr.ecr.us-east-1.amazonaws.com/my-repo/custom-alpine:3.21`).
##### Optional: harden the trust policy
Daytona sends a `daytona--pull` session name on every AssumeRole call. You can require it in your trust policy for CloudTrail audit visibility — add inside `Condition`:
```json
"StringLike": {
"sts:RoleSessionName": "daytona--*"
}
```
### Declarative builder
[Declarative Builder](https://www.daytona.io/docs/en/declarative-builder.md) provides a powerful, code-first approach to defining dependencies for Daytona Sandboxes. Instead of importing images from a container registry, you can programmatically define them using the Daytona [SDKs](https://www.daytona.io/docs/en/getting-started.md#sdks).
### Resources
Snapshots can be customized with specific [sandbox resources](https://www.daytona.io/docs/en/sandboxes.md#resources). By default, Daytona sandboxes use **1 vCPU**, **1GB RAM**, and **3GiB disk**. To view your available resources and limits, see [limits](https://www.daytona.io/docs/en/limits.md) or navigate to [Daytona Limits ↗](https://app.daytona.io/dashboard/limits).
To set custom snapshot resources, use the `Resources` class.
```python
from daytona import Daytona, CreateSnapshotParams, Resources
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(
name="my-awesome-snapshot",
image="python:3.12",
resources=Resources(cpu=2, memory=4, disk=8),
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-awesome-snapshot1123",
image: "python:3.12",
resources: { cpu: 2, memory: 4, disk: 8 },
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-awesome-snapshot',
image: 'python:3.12',
resources: Daytona::Resources.new(
cpu: 2,
memory: 4,
disk: 8
)
)
)
```
```go
package main
import (
"context"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-awesome-snapshot",
Image: "python:3.12",
Resources: &types.Resources{
CPU: 2,
Memory: 4,
Disk: 8,
},
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
import io.daytona.sdk.model.Resources;
import io.daytona.sdk.model.Snapshot;
final class CreateSnapshotResources {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Resources resources = new Resources();
resources.setCpu(2);
resources.setMemory(4);
resources.setDisk(8);
Snapshot snapshot = daytona.snapshot().create(
"my-awesome-snapshot",
Image.base("python:3.12"),
resources,
null
);
}
}
}
```
```bash
daytona snapshot create my-awesome-snapshot --image python:3.11-slim --cpu 2 --memory 4 --disk 8
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-awesome-snapshot",
"imageName": "python:3.11-slim",
"cpu": 2,
"memory": 4,
"disk": 8
}'
```
### Regions
When creating a snapshot, you can specify the [region](https://www.daytona.io/docs/en/regions.md) in which it will be available. If not specified, the snapshot will be created in your organization's default region. When you later create a sandbox from this snapshot, you can use the snapshot's region as the target region for the sandbox.
```python
from daytona import Daytona, CreateSnapshotParams
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(
name="my-awesome-snapshot",
image="python:3.12",
region_id="eu",
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-awesome-snapshotus",
image: "python:3.12",
regionId: "us",
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-awesome-snapshot',
image: 'python:3.12',
region_id: 'us'
)
)
```
```bash
daytona snapshot create my-awesome-snapshot --image python:3.11-slim --region us
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-awesome-snapshot",
"imageName": "python:3.11-slim",
"regionId": "us"
}'
```
## Get a Snapshot by name
Daytona provides an option to get a snapshot by name.
The following snippet returns the snapshot with the specified name:
```python
daytona.snapshot.get("my-awesome-snapshot")
```
```typescript
await daytona.snapshot.get('my-awesome-snapshot')
```
```ruby
daytona.snapshot.get('my-awesome-snapshot')
```
```go
_, err := client.Snapshots.Get(ctx, "my-awesome-snapshot")
```
```java
daytona.snapshot().get("my-awesome-snapshot");
```
```bash
curl https://app.daytona.io/api/snapshots/my-awesome-snapshot \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## List Snapshots
Daytona provides options to list snapshots and view their details.
The following snippet lists all snapshots on the first page with a limit of 10 snapshots per page.
```python
daytona.snapshot.list(page=2, limit=10)
```
```typescript
await daytona.snapshot.list(2, 10)
```
```ruby
daytona.snapshot.list(page: 2, limit: 10)
```
```go
page, limit := 2, 10
_, err := client.Snapshots.List(ctx, &page, &limit)
```
```java
daytona.snapshot().list(2, 10);
```
```bash
# List snapshots with pagination
daytona snapshot list --page 2 --limit 10
```
```bash
curl 'https://app.daytona.io/api/snapshots?page=2&limit=10' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Activate Snapshots
Snapshots automatically become inactive after 2 weeks of not being used. To activate an inactive snapshot:
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to activate
3. Click the **Activate** button
```python
daytona.snapshot.activate("my-awesome-snapshot")
```
```typescript
await daytona.snapshot.activate("my-awesome-snapshot")
```
```ruby
daytona.snapshot.activate('my-awesome-snapshot')
```
```bash
curl https://app.daytona.io/api/snapshots/my-inactive-snapshot/activate \
--request POST \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Deactivate Snapshots
Daytona provides an option to deactivate snapshots. Deactivated snapshots are not available for new sandboxes.
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to deactivate
3. Click the **Deactivate** button
## Delete Snapshots
Daytona provides options to delete snapshots. Deleted snapshots cannot be recovered.
1. Navigate to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to delete
3. Click the **Delete** button
```python
daytona.snapshot.delete(daytona.snapshot.get("my-awesome-snapshot"))
```
```typescript
await daytona.snapshot.delete(await daytona.snapshot.get("my-awesome-snapshot"))
```
```ruby
daytona.snapshot.delete(daytona.snapshot.get('my-awesome-snapshot'))
```
```go
snapshot, err := client.Snapshots.Get(ctx, "my-awesome-snapshot")
err = client.Snapshots.Delete(ctx, snapshot)
```
```java
daytona.snapshot().delete(daytona.snapshot().get("my-awesome-snapshot").getId());
```
```bash
daytona snapshot delete my-awesome-snapshot
```
```bash
curl https://app.daytona.io/api/snapshots/my-awesome-snapshot \
--request DELETE \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Snapshot lifecycle
A snapshot can have several different states. Each state reflects the snapshot's current status.
- **Pending**: the snapshot creation has been requested
- **Building**: the snapshot is being built
- **Pulling**: the snapshot image is being pulled from a registry
- **Active**: the snapshot is ready to use for creating sandboxes
- **Inactive**: the snapshot is deactivated
- **Error**: the snapshot creation failed
- **Build Failed**: the snapshot build process failed
- **Removing**: the snapshot is being deleted
:::note
Inactive snapshots cannot be used to create sandboxes. They must be explicitly [re-activated](#activate-snapshots) before use. When activated, the snapshot returns to `pending` state and is re-processed before becoming `active` again.
:::
## Run Docker in a Sandbox
Daytona Sandboxes can run Docker containers inside them (**Docker-in-Docker**), enabling you to build, test, and deploy containerized applications. This is particularly useful when your projects have dependencies on external services like databases, message queues, or other microservices.
Agents can seamlessly interact with these services since they run within the same sandbox environment, providing better isolation and security compared to external service dependencies. The following use cases are supported:
- Run databases (PostgreSQL, Redis, MySQL) and other services
- Build and test containerized applications
- Deploy microservices and their dependencies
- Create isolated development environments with full container orchestration
:::note
Docker-in-Docker Sandboxes require additional resources due to the Docker daemon overhead. Consider allocating at least 2 vCPU and 4GiB of memory for optimal performance.
:::
#### Create a Docker-in-Docker Snapshot
Daytona provides an option to create a snapshot with Docker support using pre-built Docker-in-Docker images as a base or by manually installing Docker in a custom image.
##### Using pre-built images
The following base images are widely used for creating Docker-in-Docker snapshots or can be used as a base for a custom Dockerfile:
- `docker:28.3.3-dind`: official Docker-in-Docker image (Alpine-based, lightweight)
- `docker:28.3.3-dind-rootless`: rootless Docker-in-Docker for enhanced security
- `docker:28.3.2-dind-alpine3.22`: Docker-in-Docker image with Alpine 3.22
##### Using manual installation
Alternatively, install Docker manually in a custom Dockerfile:
```dockerfile
FROM ubuntu:22.04
# Install Docker using the official install script
RUN curl -fsSL https://get.docker.com | VERSION=28.3.3 sh -
```
#### Run Docker Compose in a Sandbox
Docker Compose allows you to define and run multi-container applications. With Docker-in-Docker enabled in a Daytona Sandbox, you can use Docker Compose to orchestrate services like databases, caches, and application containers.
First, create a Docker-in-Docker snapshot using the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/snapshots) or [CLI](https://www.daytona.io/docs/en/tools/cli.md#daytona-snapshot-create) with one of the [pre-built images](#using-pre-built-images) (e.g., `docker:28.3.3-dind`). Then use the following snippet to run Docker Compose services inside a sandbox:
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
# Initialize the Daytona client
daytona = Daytona()
# Create a sandbox from a Docker-in-Docker snapshot
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot='docker-dind'))
# Create a docker-compose.yml file
compose_content = '''
services:
web:
image: nginx:alpine
ports:
- "8080:80"
'''
sandbox.fs.upload_file(compose_content.encode(), 'docker-compose.yml')
# Start Docker Compose services
result = sandbox.process.exec('docker compose -p demo up -d')
print(result.result)
# Check running services
result = sandbox.process.exec('docker compose -p demo ps')
print(result.result)
# Clean up
sandbox.process.exec('docker compose -p demo down')
```
```typescript
import { Daytona } from '@daytona/sdk'
// Initialize the Daytona client
const daytona = new Daytona()
// Create a sandbox from a Docker-in-Docker snapshot
const sandbox = await daytona.create({ snapshot: 'docker-dind' })
// Create a docker-compose.yml file
const composeContent = `
services:
web:
image: nginx:alpine
ports:
- "8080:80"
`
await sandbox.fs.uploadFile(Buffer.from(composeContent), 'docker-compose.yml')
// Start Docker Compose services
let result = await sandbox.process.executeCommand('docker compose -p demo up -d')
console.log(result.result)
// Check running services
result = await sandbox.process.executeCommand('docker compose -p demo ps')
console.log(result.result)
// Clean up
await sandbox.process.executeCommand('docker compose -p demo down')
```
```ruby
require 'daytona'
# Initialize the Daytona client
daytona = Daytona::Daytona.new
# Create a sandbox from a Docker-in-Docker snapshot
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'docker-dind'))
# Create a docker-compose.yml file
compose_content = <<~YAML
services:
web:
image: nginx:alpine
ports:
- "8080:80"
YAML
sandbox.fs.upload_file(compose_content, 'docker-compose.yml')
# Start Docker Compose services
result = sandbox.process.exec(command: 'docker compose -p demo up -d')
puts result.result
# Check running services
result = sandbox.process.exec(command: 'docker compose -p demo ps')
puts result.result
# Clean up
sandbox.process.exec(command: 'docker compose -p demo down')
```
```go
package main
import (
"context"
"fmt"
"github.com/daytonaio/sdk-go/daytona"
"github.com/daytonaio/sdk-go/types"
)
func main() {
ctx := context.Background()
// Initialize the Daytona client
client, _ := daytona.NewDaytona(nil)
// Create a sandbox from a Docker-in-Docker snapshot
sandbox, _ := client.Create(ctx, &types.CreateSandboxFromSnapshotParams{
Snapshot: daytona.Ptr("docker-dind"),
}, nil)
// Create a docker-compose.yml file
composeContent := `
services:
web:
image: nginx:alpine
ports:
- "8080:80"
`
sandbox.Fs.UploadFile(ctx, []byte(composeContent), "docker-compose.yml")
// Start Docker Compose services
result, _ := sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo up -d", nil)
fmt.Println(result.Result)
// Check running services
result, _ = sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo ps", nil)
fmt.Println(result.Result)
// Clean up
sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo down", nil)
}
```
## Run Kubernetes in a Sandbox
Daytona Sandboxes can run a Kubernetes cluster inside the sandbox. Kubernetes runs entirely inside the sandbox and is removed when the sandbox is deleted, keeping environments secure and reproducible.
##### Run k3s in a Sandbox
The following snippet installs and starts a k3s cluster inside a sandbox and lists all running pods.
```typescript
import { Daytona } from '@daytona/sdk'
import { setTimeout } from 'timers/promises'
// Initialize the Daytona client
const daytona = new Daytona()
// Create the sandbox instance
const sandbox = await daytona.create()
// Run the k3s installation script
const response = await sandbox.process.executeCommand(
'curl -sfL https://get.k3s.io | sh -'
)
// Run k3s
const sessionName = 'k3s-server'
await sandbox.process.createSession(sessionName)
const k3s = await sandbox.process.executeSessionCommand(sessionName, {
command: 'sudo /usr/local/bin/k3s server',
async: true,
})
// Give time to k3s to fully start
await setTimeout(30000)
// Get all pods
const pods = await sandbox.process.executeCommand(
'sudo /usr/local/bin/kubectl get pod -A'
)
console.log(pods.result)
```
## Default Snapshots
Daytona provides pre-built snapshots with fixed resource sizes for creating sandboxes.
| **Snapshot** | **vCPU** | **Memory** | **Storage** | **GPU** |
| -------------------- | -------- | ---------- | ----------- | ------- |
| **`daytona-small`** | 1 | 1GiB | 3GiB | |
| **`daytona-medium`** | 2 | 4GiB | 8GiB | |
| **`daytona-large`** | 4 | 8GiB | 10GiB | |
| **`daytona-gpu`** | 1 | 1GiB | 1GiB | 1 |
| **`windows`** | 2 | 8GiB | 30GiB | |
Snapshots are based on the `daytonaio/sandbox:` image.
### Python packages (pip)
- `anthropic` (v0.76.0)
- `beautifulsoup4` (v4.14.3)
- `claude-agent-sdk` (v0.1.22)
- `openai-agents` (v0.15.1)
- `daytona` (v0.134.0)
- `django` (v6.0.1)
- `flask` (v3.1.2)
- `huggingface-hub` (v0.36.0)
- `instructor` (v1.14.4)
- `keras` (v3.13.0)
- `langchain` (v1.2.7)
- `llama-index` (v0.14.13)
- `matplotlib` (v3.10.8)
- `numpy` (v2.4.1)
- `ollama` (v0.6.1)
- `openai` (v2.33.0)
- `opencv-python` (v4.13.0.90)
- `pandas` (v2.3.3)
- `pillow` (v12.1.0)
- `pipx` (v1.8.0)
- `pydantic-ai` (v1.47.0)
- `python-lsp-server` (v1.14.0)
- `requests` (v2.32.5)
- `scikit-learn` (v1.8.0)
- `scipy` (v1.17.0)
- `seaborn` (v0.13.2)
- `sqlalchemy` (v2.0.46)
- `torch` (v2.10.0)
- `transformers` (v4.57.6)
- `uv` (v0.9.26)
### Node.js packages (npm)
- `@anthropic-ai/claude-code` (v2.1.19)
- `@openai/codex` (0.128.0)
- `bun` (v1.3.6)
- `openclaw` (v2026.2.1)
- `opencode-ai` (v1.1.35)
- `ts-node` (v10.9.2)
- `typescript` (v5.9.3)
- `typescript-language-server` (v5.1.3)
# Declarative Builder
Declarative Builder provides a powerful, code-first approach to defining dependencies for Daytona Sandboxes. Instead of importing images from a container registry, you can programmatically define them using the Daytona SDK.
The declarative builder system supports two primary workflows:
- [**Declarative images**](#build-declarative-images): build images on demand when creating sandboxes
- [**Pre-built snapshots**](#create-pre-built-snapshots): create and register ready-to-use [snapshots](https://www.daytona.io/docs/snapshots.md)
## Build declarative images
Daytona provides an option to create declarative images on-the-fly when creating sandboxes. This is ideal for iterating quickly without creating separate snapshots.
Declarative images are cached for 24 hours, and are automatically reused when running the same script. Thus, subsequent runs on the same runner will be almost instantaneous.
```python
# Define a declarative image with python packages
declarative_image = (
Image.debian_slim("3.12")
.pip_install(["requests", "pytest"])
.workdir("/home/daytona")
)
# Create a new sandbox with the declarative image and stream the build logs
sandbox = daytona.create(
CreateSandboxFromImageParams(image=declarative_image),
timeout=0,
on_snapshot_create_logs=print,
)
```
```typescript
// Define a declarative image with python packages
const declarativeImage = Image.debianSlim('3.12')
.pipInstall(['requests', 'pytest'])
.workdir('/home/daytona')
// Create a new sandbox with the declarative image and stream the build logs
const sandbox = await daytona.create(
{
image: declarativeImage,
},
{
timeout: 0,
onSnapshotCreateLogs: console.log,
}
)
```
```ruby
# Define a simple declarative image with Python packages
declarative_image = Daytona::Image
.debian_slim('3.12')
.pip_install(['requests', 'pytest'])
.workdir('/home/daytona')
# Create a new Sandbox with the declarative image and stream the build logs
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(image: declarative_image),
on_snapshot_create_logs: proc { |chunk| puts chunk }
)
```
```go
// Define a declarative image with python packages
version := "3.12"
declarativeImage := daytona.DebianSlim(&version).
PipInstall([]string{"requests", "pytest"}).
Workdir("/home/daytona")
// Create a new sandbox with the declarative image and stream the build logs
logChan := make(chan string)
go func() {
for log := range logChan {
fmt.Print(log)
}
}()
sandbox, err := client.Create(ctx, types.ImageParams{
Image: declarativeImage,
}, options.WithTimeout(0), options.WithLogChannel(logChan))
if err != nil {
// handle error
}
```
```java
// Define a declarative image with python packages
Image declarativeImage = Image.debianSlim("3.12")
.pipInstall("requests", "pytest")
.workdir("/home/daytona");
// Create a new sandbox with the declarative image and stream the build logs
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage(declarativeImage);
Sandbox sandbox = daytona.create(params, 0L, System.out::println);
```
:::note
Use the following best practices when working with the declarative builder:
- **Layer Optimization**: Group related operations to minimize Docker layers
- **Cache Utilization**: Identical build commands and context will be cached and subsequent builds will be almost instant
- **Security**: Create non-root users for application workloads
- **Resource Efficiency**: Use slim base images when appropriate
- **Context Minimization**: Only include necessary files in the build context
:::
## Create pre-built Snapshots
Daytona provides an option to [create pre-built snapshots](https://www.daytona.io/docs/snapshots.md#create-snapshots) that can be reused across multiple sandboxes.
The snapshot remains visible in the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/snapshots) and is permanently cached, ensuring instant availability without rebuilding.
```python
# Create a python data science image
snapshot_name = "data-science-snapshot"
image = (
Image.debian_slim("3.12")
.pip_install(["pandas", "numpy"])
.workdir("/home/daytona")
)
# Create the snapshot and stream the build logs
daytona.snapshot.create(
CreateSnapshotParams(
name=snapshot_name,
image=image,
),
on_logs=print,
)
# Create a new sandbox using the pre-built snapshot
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(snapshot=snapshot_name)
)
```
```typescript
// Create a python data science image
const snapshotName = 'data-science-snapshot'
const image = Image.debianSlim('3.12')
.pipInstall(['pandas', 'numpy'])
.workdir('/home/daytona')
// Create the snapshot and stream the build logs
await daytona.snapshot.create(
{
name: snapshotName,
image,
},
{
onLogs: console.log,
}
)
// Create a new sandbox using the pre-built snapshot
const sandbox = await daytona.create({
snapshot: snapshotName,
})
```
```ruby
# Create a simple Python data science image
snapshot_name = 'data-science-snapshot'
image = Daytona::Image
.debian_slim('3.12')
.pip_install(['pandas', 'numpy'])
.workdir('/home/daytona')
# Create the Snapshot and stream the build logs
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: snapshot_name,
image: image
),
on_logs: proc { |chunk| puts chunk }
)
# Create a new Sandbox using the pre-built Snapshot
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(snapshot: snapshot_name)
)
```
```go
// Create a python data science image
snapshotName := "data-science-snapshot"
version := "3.12"
image := daytona.DebianSlim(&version).
PipInstall([]string{"pandas", "numpy"}).
Workdir("/home/daytona")
// Create the snapshot and stream the build logs
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: snapshotName,
Image: image,
})
if err != nil {
// handle error
}
for log := range logChan {
fmt.Print(log)
}
// Create a new sandbox using the pre-built snapshot
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: snapshotName,
})
if err != nil {
// handle error
}
```
```java
// Create a python data science image
String snapshotName = "data-science-snapshot";
Image image = Image.debianSlim("3.12")
.pipInstall("pandas", "numpy")
.workdir("/home/daytona");
// Create the snapshot and stream the build logs
daytona.snapshot().create(snapshotName, image, System.out::println);
// Create a new sandbox using the pre-built snapshot
CreateSandboxFromSnapshotParams snapshotParams = new CreateSandboxFromSnapshotParams();
snapshotParams.setSnapshot(snapshotName);
Sandbox sandbox = daytona.create(snapshotParams);
```
## Image configuration
Daytona provides an option to define images programmatically using the Daytona SDK. You can specify base images, install packages, add files, set environment variables, and more.
For a complete API reference and method signatures, see the [Python](https://www.daytona.io/docs/python-sdk/common/image.md), [TypeScript](https://www.daytona.io/docs/typescript-sdk/image.md), [Ruby](https://www.daytona.io/docs/ruby-sdk/image.md), [Go](https://www.daytona.io/docs/go-sdk/daytona.md#type-DockerImage), and [Java](https://www.daytona.io/docs/java-sdk/image.md) SDK references.
### Base image selection
Daytona provides an option to select base images. The following snippets demonstrate how to select and configure base images:
```python
# Create an image from a base
image = Image.base("python:3.12-slim-bookworm")
# Use a Debian slim image with Python 3.12
image = Image.debian_slim("3.12")
```
```typescript
// Create an image from a base
const image = Image.base('python:3.12-slim-bookworm')
// Use a Debian slim image with Python 3.12
const image = Image.debianSlim('3.12')
```
```ruby
# Create an image from a base
image = Daytona::Image.base('python:3.12-slim-bookworm')
# Use a Debian slim image with Python 3.12
image = Daytona::Image.debian_slim('3.12')
```
```go
// Create an image from a base
image := daytona.Base("python:3.12-slim-bookworm")
// Use a Debian slim image with Python 3.12
version := "3.12"
image := daytona.DebianSlim(&version)
```
```java
// Create an image from a base
Image image = Image.base("python:3.12-slim-bookworm");
// Use a Debian slim image with Python 3.12
image = Image.debianSlim("3.12");
```
### Package management
Daytona provides an option to install packages and dependencies to your image.
The following snippets demonstrate how to install packages and dependencies to your image:
```python
# Add pip packages
image = Image.debian_slim("3.12").pip_install(["requests", "pandas"])
# Install from requirements.txt
image = Image.debian_slim("3.12").pip_install_from_requirements("requirements.txt")
# Install from pyproject.toml (with optional dependencies)
image = Image.debian_slim("3.12").pip_install_from_pyproject("pyproject.toml", optional_dependencies=["dev"])
```
```typescript
// Add pip packages
const image = Image.debianSlim('3.12').pipInstall(['requests', 'pandas'])
// Install from requirements.txt
const image = Image.debianSlim('3.12').pipInstallFromRequirements('requirements.txt')
// Install from pyproject.toml (with optional dependencies)
const image = Image.debianSlim('3.12').pipInstallFromPyproject('pyproject.toml', {
optionalDependencies: ['dev']
})
```
```ruby
# Add pip packages
image = Daytona::Image.debian_slim('3.12').pip_install(['requests', 'pandas'])
# Install from requirements.txt
image = Daytona::Image.debian_slim('3.12').pip_install_from_requirements('requirements.txt')
# Install from pyproject.toml (with optional dependencies)
image = Daytona::Image.debian_slim('3.12').pip_install_from_pyproject('pyproject.toml',
optional_dependencies: ['dev']
)
```
```go
// Add pip packages
version := "3.12"
image := daytona.DebianSlim(&version).PipInstall([]string{"requests", "pandas"})
// Install from requirements.txt
image := daytona.DebianSlim(&version).
AddLocalFile("requirements.txt", "/tmp/requirements.txt").
Run("pip install -r /tmp/requirements.txt")
// Install from pyproject.toml (with optional dependencies)
image := daytona.DebianSlim(&version).
AddLocalFile("pyproject.toml", "/tmp/pyproject.toml").
Run("pip install /tmp[dev]")
```
```java
// Add pip packages
Image image = Image.debianSlim("3.12").pipInstall("requests", "pandas");
```
### File system operations
Daytona provides an option to add files and directories to your image.
The following snippets demonstrate how to add files and directories to your image:
```python
# Add a local file
image = Image.debian_slim("3.12").add_local_file("package.json", "/home/daytona/package.json")
# Add a local directory
image = Image.debian_slim("3.12").add_local_dir("src", "/home/daytona/src")
```
```typescript
// Add a local file
const image = Image.debianSlim('3.12').addLocalFile('package.json', '/home/daytona/package.json')
// Add a local directory
const image = Image.debianSlim('3.12').addLocalDir('src', '/home/daytona/src')
```
```ruby
# Add a local file
image = Daytona::Image.debian_slim('3.12').add_local_file('package.json', '/home/daytona/package.json')
# Add a local directory
image = Daytona::Image.debian_slim('3.12').add_local_dir('src', '/home/daytona/src')
```
```go
// Add a local file
version := "3.12"
image := daytona.DebianSlim(&version).AddLocalFile("package.json", "/home/daytona/package.json")
// Add a local directory
image := daytona.DebianSlim(&version).AddLocalDir("src", "/home/daytona/src")
```
### Environment configuration
Daytona provides an option to configure environment variables and working directories.
The following snippets demonstrate how to configure environment variables and working directories:
```python
# Set environment variables
image = Image.debian_slim("3.12").env({"PROJECT_ROOT": "/home/daytona"})
# Set working directory
image = Image.debian_slim("3.12").workdir("/home/daytona")
```
```typescript
// Set environment variables
const image = Image.debianSlim('3.12').env({ PROJECT_ROOT: '/home/daytona' })
// Set working directory
const image = Image.debianSlim('3.12').workdir('/home/daytona')
```
```ruby
# Set environment variables
image = Daytona::Image.debian_slim('3.12').env({ 'PROJECT_ROOT' => '/home/daytona' })
# Set working directory
image = Daytona::Image.debian_slim('3.12').workdir('/home/daytona')
```
```go
// Set environment variables
version := "3.12"
image := daytona.DebianSlim(&version).Env("PROJECT_ROOT", "/home/daytona")
// Set working directory
image := daytona.DebianSlim(&version).Workdir("/home/daytona")
```
```java
// Set environment variables
Image image = Image.debianSlim("3.12")
.env(java.util.Map.of("PROJECT_ROOT", "/home/daytona"));
// Set working directory
image = Image.debianSlim("3.12").workdir("/home/daytona");
```
### Commands and entrypoints
Daytona provides an option to execute commands during build and configure container startup behavior.
The following snippets demonstrate how to execute commands during build and configure container startup behavior:
```python
# Run shell commands during build
image = Image.debian_slim("3.12").run_commands(
'apt-get update && apt-get install -y git',
'groupadd -r daytona && useradd -r -g daytona -m daytona',
'mkdir -p /home/daytona/workspace'
)
# Set entrypoint
image = Image.debian_slim("3.12").entrypoint(["/bin/bash"])
# Set default command
image = Image.debian_slim("3.12").cmd(["/bin/bash"])
```
```typescript
// Run shell commands during build
const image = Image.debianSlim('3.12').runCommands(
'apt-get update && apt-get install -y git',
'groupadd -r daytona && useradd -r -g daytona -m daytona',
'mkdir -p /home/daytona/workspace'
)
// Set entrypoint
const image = Image.debianSlim('3.12').entrypoint(['/bin/bash'])
// Set default command
const image = Image.debianSlim('3.12').cmd(['/bin/bash'])
```
```ruby
# Run shell commands during build
image = Daytona::Image.debian_slim('3.12').run_commands(
'apt-get update && apt-get install -y git',
'groupadd -r daytona && useradd -r -g daytona -m daytona',
'mkdir -p /home/daytona/workspace'
)
# Set entrypoint
image = Daytona::Image.debian_slim('3.12').entrypoint(['/bin/bash'])
# Set default command
image = Daytona::Image.debian_slim('3.12').cmd(['/bin/bash'])
```
```go
// Run shell commands during build
version := "3.12"
image := daytona.DebianSlim(&version).
Run("apt-get update && apt-get install -y git").
Run("groupadd -r daytona && useradd -r -g daytona -m daytona").
Run("mkdir -p /home/daytona/workspace")
// Set entrypoint
image := daytona.DebianSlim(&version).Entrypoint([]string{"/bin/bash"})
// Set default command
image := daytona.DebianSlim(&version).Cmd([]string{"/bin/bash"})
```
```java
// Run shell commands during build
Image image = Image.debianSlim("3.12").runCommands(
"apt-get update && apt-get install -y git",
"groupadd -r daytona && useradd -r -g daytona -m daytona",
"mkdir -p /home/daytona/workspace"
);
// Set entrypoint
image = Image.debianSlim("3.12").entrypoint("/bin/bash");
// Set default command
image = Image.debianSlim("3.12").cmd("/bin/bash");
```
### Dockerfile integration
Daytona provides an option to integrate existing Dockerfiles or add custom Dockerfile commands.
The following snippets demonstrate how to integrate existing Dockerfiles or add custom Dockerfile commands:
```python
# Add custom Dockerfile commands
image = Image.debian_slim("3.12").dockerfile_commands(["RUN echo 'Hello, world!'"])
# Use an existing Dockerfile
image = Image.from_dockerfile("Dockerfile")
# Extend an existing Dockerfile
image = Image.from_dockerfile("app/Dockerfile").pip_install(["numpy"])
```
```typescript
// Add custom Dockerfile commands
const image = Image.debianSlim('3.12').dockerfileCommands(['RUN echo "Hello, world!"'])
// Use an existing Dockerfile
const image = Image.fromDockerfile('Dockerfile')
// Extend an existing Dockerfile
const image = Image.fromDockerfile("app/Dockerfile").pipInstall(['numpy'])
```
```ruby
# Add custom Dockerfile commands
image = Daytona::Image.debian_slim('3.12').dockerfile_commands(['RUN echo "Hello, world!"'])
# Use an existing Dockerfile
image = Daytona::Image.from_dockerfile('Dockerfile')
# Extend an existing Dockerfile
image = Daytona::Image.from_dockerfile('app/Dockerfile').pip_install(['numpy'])
```
```go
// Note: In Go, FromDockerfile takes the Dockerfile content as a string
content, err := os.ReadFile("Dockerfile")
if err != nil {
// handle error
}
image := daytona.FromDockerfile(string(content))
// Extend an existing Dockerfile with additional commands
content, err = os.ReadFile("app/Dockerfile")
if err != nil {
// handle error
}
image := daytona.FromDockerfile(string(content)).
PipInstall([]string{"numpy"})
```
### System package installation
Daytona provides an option to install OS-level packages during the image build. Use this pattern when your sandbox needs CLI tools or system libraries that are not available through `pip`.
Each string passed to `run_commands` becomes a separate Dockerfile `RUN` instruction, and every `RUN` produces an immutable layer. To keep the image small, chain the package install and the apt cache cleanup together with `&&` inside a single string so the cache is never persisted in any layer.
```python
image = Image.debian_slim("3.12").run_commands(
"apt-get update "
"&& apt-get install -y --no-install-recommends git curl ffmpeg jq "
"&& rm -rf /var/lib/apt/lists/*"
)
```
```typescript
const image = Image.debianSlim('3.12').runCommands(
'apt-get update ' +
'&& apt-get install -y --no-install-recommends git curl ffmpeg jq ' +
'&& rm -rf /var/lib/apt/lists/*',
)
```
```ruby
image = Daytona::Image
.debian_slim('3.12')
.run_commands(
'apt-get update ' \
'&& apt-get install -y --no-install-recommends git curl ffmpeg jq ' \
'&& rm -rf /var/lib/apt/lists/*'
)
```
```go
version := "3.12"
image := daytona.DebianSlim(&version).
AptGet([]string{"git", "curl", "ffmpeg", "jq"})
```
```java
Image image = Image.debianSlim("3.12").runCommands(
"apt-get update "
+ "&& apt-get install -y --no-install-recommends git curl ffmpeg jq "
+ "&& rm -rf /var/lib/apt/lists/*"
);
```
### Non-root user setup
Daytona provides an option to define a non-root user for application workloads. Run all installation steps as `root` first, then create the user, fix ownership of the working directory, and switch to the new user with the `USER` directive. Subsequent commands and the sandbox runtime then operate without root privileges.
Place all installation steps before the `USER` directive. After switching to the non-root user, commands that write to system locations (such as `apt-get install` or `pip install` without `--user`) will fail with permission errors.
```python
image = (
Image.debian_slim("3.12")
.pip_install(["fastapi", "uvicorn"])
.run_commands(
"groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona",
"chown -R daytona:daytona /home/daytona",
)
.workdir("/home/daytona")
.dockerfile_commands(["USER daytona"])
)
```
```typescript
const image = Image.debianSlim('3.12')
.pipInstall(['fastapi', 'uvicorn'])
.runCommands(
'groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona',
'chown -R daytona:daytona /home/daytona',
)
.workdir('/home/daytona')
.dockerfileCommands(['USER daytona'])
```
```ruby
image = Daytona::Image
.debian_slim('3.12')
.pip_install(['fastapi', 'uvicorn'])
.run_commands(
'groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona',
'chown -R daytona:daytona /home/daytona'
)
.workdir('/home/daytona')
.dockerfile_commands(['USER daytona'])
```
```go
version := "3.12"
image := daytona.DebianSlim(&version).
PipInstall([]string{"fastapi", "uvicorn"}).
Run("groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona").
Run("chown -R daytona:daytona /home/daytona").
Workdir("/home/daytona").
User("daytona")
```
### Multi-language runtimes
Daytona provides an option to combine multiple language runtimes in a single image. The following pattern adds Node.js 20 to a Python base image by installing it from the NodeSource repository. The same approach works for adding Go, Ruby, Java, or any other runtime that distributes a Linux installer.
Chain the apt operations, the NodeSource installer, and the cache cleanup into a single `RUN` instruction. If the cache cleanup runs in a separate `RUN`, the apt cache is already persisted in the earlier layers and the final image keeps those bytes.
```python
image = (
Image.debian_slim("3.12")
.run_commands(
"apt-get update "
"&& apt-get install -y --no-install-recommends curl ca-certificates "
"&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - "
"&& apt-get install -y nodejs "
"&& rm -rf /var/lib/apt/lists/*"
)
.pip_install(["fastapi", "uvicorn"])
)
```
```typescript
const image = Image.debianSlim('3.12')
.runCommands(
'apt-get update ' +
'&& apt-get install -y --no-install-recommends curl ca-certificates ' +
'&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - ' +
'&& apt-get install -y nodejs ' +
'&& rm -rf /var/lib/apt/lists/*',
)
.pipInstall(['fastapi', 'uvicorn'])
```
```ruby
image = Daytona::Image
.debian_slim('3.12')
.run_commands(
'apt-get update ' \
'&& apt-get install -y --no-install-recommends curl ca-certificates ' \
'&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - ' \
'&& apt-get install -y nodejs ' \
'&& rm -rf /var/lib/apt/lists/*'
)
.pip_install(['fastapi', 'uvicorn'])
```
```go
version := "3.12"
image := daytona.DebianSlim(&version).
Run("apt-get update " +
"&& apt-get install -y --no-install-recommends curl ca-certificates " +
"&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - " +
"&& apt-get install -y nodejs " +
"&& rm -rf /var/lib/apt/lists/*").
PipInstall([]string{"fastapi", "uvicorn"})
```
```java
Image image = Image.debianSlim("3.12")
.runCommands(
"apt-get update "
+ "&& apt-get install -y --no-install-recommends curl ca-certificates "
+ "&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - "
+ "&& apt-get install -y nodejs "
+ "&& rm -rf /var/lib/apt/lists/*"
)
.pipInstall("fastapi", "uvicorn");
```
# Volumes
Volumes are FUSE-based mounts that provide shared file access across Daytona sandboxes. They enable sandboxes to read from large files instantly - no need to upload files manually to each sandbox. Volume data is stored in an S3-compatible object store.
A sandbox reads and writes a mounted volume like any local directory, and the contents persist independently of the sandbox lifecycle. Use volumes to share datasets, model weights, build caches, or application state between sandboxes, scope per-user or per-tenant data with a `subpath`, and combine multiple volumes in the same sandbox at different mount paths.
- multiple volumes can be mounted to a single sandbox
- a single volume can be mounted to multiple sandboxes
## Create volumes
Daytona provides methods to create volumes using the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/volumes) or programmatically using the Daytona [Python](https://www.daytona.io/docs/en/python-sdk/sync/volume.md), [TypeScript](https://www.daytona.io/docs/en/typescript-sdk/volume.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk/volume.md), [Go](https://www.daytona.io/docs/en/go-sdk/daytona.md#type-volumeservice), [Java](https://www.daytona.io/docs/en/java-sdk/volume-service.md) **SDKs**, [CLI](https://www.daytona.io/docs/en/tools/cli.md#daytona-create), or [API](https://www.daytona.io/docs/en/tools/api.md#daytona/tag/sandbox).
For persistent per-user, per-tenant, or per-workspace storage, use one shared volume per use case, environment, or project (for example a volume for staging and another for production), and set a dedicated `subpath` when you create each sandbox. The sandbox sees only that prefix inside the volume; it cannot access sibling subpaths.
This is the default pattern we recommend because it:
- stays within the per-organization volume [limits](#pricing--limits)
- avoids mounting a separate volume for every user or sandbox
- continues to provide strong isolation at the mount boundary
1. Navigate to [Daytona Volumes ↗](https://app.daytona.io/dashboard/volumes)
2. Click the **Create Volume** button
3. Enter the volume name
```python
from daytona import Daytona
daytona = Daytona()
volume = daytona.volume.create("my-awesome-volume")
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const volume = await daytona.volume.create("my-awesome-volume");
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.create("my-awesome-volume")
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Create(context.Background(), "my-awesome-volume")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Volume ID: %s\n", volume.ID)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.model.Volume;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume = daytona.volume().create("my-awesome-volume");
}
}
}
```
```shell
daytona volume create my-awesome-volume
```
```bash
curl 'https://app.daytona.io/api/volumes' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-awesome-volume"
}'
```
## Mount volumes
Daytona provides an option to mount a volume to a sandbox. Once a volume is created, it can be mounted to a sandbox by specifying it in the `CreateSandboxFromSnapshotParams` object. For per-user or multi-tenant data, pass `subpath` so only the specified folder inside the volume is visible at `mount_path`.
Mount the entire volume (omit `subpath`) when every sandbox that uses that volume should see the same tree, for example shared assets or single-tenant workloads.
Volume mount paths must meet the following requirements:
- **Must be absolute paths**: mount paths must start with `/` (e.g., `/home/daytona/volume`)
- **Cannot be root directory**: cannot mount to `/` or `//`
- **No relative path components**: cannot contain `/../`, `/./`, or end with `/..` or `/.`
- **No consecutive slashes**: cannot contain multiple consecutive slashes like `//` (except at the beginning)
- **Cannot mount to system directories**: the following system directories are prohibited: `/proc`, `/sys`, `/dev`, `/boot`, `/etc`, `/bin`, `/sbin`, `/lib`, `/lib64`
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
# Create a new volume or get an existing one
volume = daytona.volume.get("my-awesome-volume", create=True)
mount_dir = "/home/daytona/volume"
# Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
params = CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/alice")],
)
sandbox = daytona.create(params)
# Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
params_full = CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
)
sandbox_shared = daytona.create(params_full)
```
```typescript
import { Daytona } from '@daytona/sdk'
import path from 'path'
const daytona = new Daytona()
const volume = await daytona.volume.get('my-awesome-volume', true)
const mountDir = '/home/daytona/volume'
// Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
const sandbox = await daytona.create({
language: 'typescript',
volumes: [
{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/alice' },
],
})
// Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
const sandboxShared = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.get('my-awesome-volume', create: true)
mount_dir = '/home/daytona/volume'
# Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id,
mount_path: mount_dir,
subpath: 'users/alice'
)]
)
sandbox = daytona.create(params)
# Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
params_shared = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(volume_id: volume.id, mount_path: mount_dir)]
)
sandbox_shared = daytona.create(params_shared)
```
```go
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// Create a new volume or get an existing one
volume, err := client.Volume.Get(context.Background(), "my-awesome-volume")
if err != nil {
// If volume doesn't exist, create it
volume, err = client.Volume.Create(context.Background(), "my-awesome-volume")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/volume"
// Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
subpath := "users/alice"
sandbox, err := client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &subpath},
},
},
})
if err != nil {
log.Fatal(err)
}
// Entire volume at mount path (omit Subpath) when all sandboxes should share the same tree
_, err = client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir},
},
},
})
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.exception.DaytonaNotFoundException;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.Volume;
import io.daytona.sdk.model.VolumeMount;
import java.util.Collections;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume;
try {
volume = daytona.volume().getByName("my-awesome-volume");
} catch (DaytonaNotFoundException e) {
volume = daytona.volume().create("my-awesome-volume");
}
String mountDir = "/home/daytona/volume";
// io.daytona.sdk.model.VolumeMount has no subpath field; use the API for subpath mounts.
// Mount the entire volume at mountPath:
CreateSandboxFromSnapshotParams paramsFull = new CreateSandboxFromSnapshotParams();
paramsFull.setLanguage("python");
VolumeMount mountFull = new VolumeMount();
mountFull.setVolumeId(volume.getId());
mountFull.setMountPath(mountDir);
paramsFull.setVolumes(Collections.singletonList(mountFull));
Sandbox sandboxShared = daytona.create(paramsFull);
}
}
}
```
```shell
daytona volume create my-awesome-volume
daytona create --volume my-awesome-volume:/home/daytona/volume
```
The `--volume` flag accepts `VOLUME_ID_OR_NAME:MOUNT_PATH` only. For a **subpath** mount, use a [SDK](#mount-volumes) or the [API](#mount-volumes) and set `subpath` on the volume entry.
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/volume",
"subpath": "users/alice"
}
]
}'
```
Omit `subpath` to mount the full volume at `mountPath`.
## Work with volumes
Daytona provides an option to read from and write to the volume just like any other directory in the sandbox file system. Files written to the volume persist beyond the lifecycle of any individual sandbox.
The following snippet demonstrate how to read from and write to a volume:
```python
# Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.upload_file(b"Hello from Daytona volume!", "/home/daytona/volume/example.txt")
# When you're done with the sandbox, you can remove it
# The volume will persist even after the sandbox is removed
sandbox.delete()
```
```typescript
// Write to a file in the mounted volume using the Sandbox file system API
await sandbox.fs.uploadFile(
Buffer.from('Hello from Daytona volume!'),
'/home/daytona/volume/example.txt'
)
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
await daytona.delete(sandbox)
```
```ruby
# Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.upload_file('Hello from Daytona volume!', '/home/daytona/volume/example.txt')
# When you're done with the sandbox, you can remove it
# The volume will persist even after the sandbox is removed
daytona.delete(sandbox)
```
```go
import (
"context"
"log"
)
// Write to a file in the mounted volume
err := sandbox.FileSystem.UploadFile(context.Background(), []byte("Hello from Daytona volume!"), "/home/daytona/volume/example.txt")
if err != nil {
log.Fatal(err)
}
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
err = sandbox.Delete(context.Background())
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
// Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.uploadFile(
"Hello from Daytona volume!".getBytes(StandardCharsets.UTF_8),
"/home/daytona/volume/example.txt");
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
sandbox.delete();
```
## Get a volume by name
Daytona provides an option to get a volume by its name.
```python
daytona.volume.get("my-awesome-volume", create=True)
```
```typescript
await daytona.volume.get('my-awesome-volume', true)
```
```ruby
daytona.volume.get('my-awesome-volume', create: true)
```
```go
volume, err := client.Volume.Get(ctx, "my-awesome-volume")
```
```java
daytona.volume().getByName("my-awesome-volume");
```
```shell
daytona volume get my-awesome-volume
```
```bash
curl 'https://app.daytona.io/api/volumes/by-name/my-awesome-volume' \
--header 'Authorization: Bearer '
```
## List volumes
Daytona provides an option to list all volumes.
```python
daytona.volume.list()
```
```typescript
await daytona.volume.list()
```
```ruby
daytona.volume.list
```
```go
volumes, err := client.Volume.List(ctx)
```
```java
daytona.volume().list();
```
```shell
daytona volume list
```
```bash
curl 'https://app.daytona.io/api/volumes' \
--header 'Authorization: Bearer '
```
## Delete volumes
Daytona provides an option to delete a volume. Deleted volumes cannot be recovered.
The following snippet demonstrate how to delete a volume:
```python
daytona.volume.delete(volume)
```
```typescript
await daytona.volume.delete(volume)
```
```ruby
daytona.volume.delete(volume)
```
```go
err := client.Volume.Delete(ctx, volume)
```
```java
daytona.volume().delete(volume.getId());
```
```shell
daytona volume delete
```
```bash
curl 'https://app.daytona.io/api/volumes/' \
--request DELETE \
--header 'Authorization: Bearer '
```
## Share data between sandboxes
Daytona provides an option to share data across sandboxes by mounting the same volume in each one. A producer sandbox writes to the volume and is then deleted; a separately created consumer sandbox mounts the same volume by ID and reads the data. Volume contents persist independently of any individual sandbox.
Sandboxes that mount the same volume see writes immediately, but FUSE-backed volumes are not transactional. If two sandboxes write to the same path concurrently, the last write wins. Coordinate access in your application when ordering matters.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("shared-data", create=True)
mount_dir = "/home/daytona/volume"
# Producer: write data into the volume, then delete the sandbox
producer = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
))
producer.fs.upload_file(b"shared payload", f"{mount_dir}/payload.bin")
producer.delete()
# Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
))
data = consumer.fs.download_file(f"{mount_dir}/payload.bin")
print(data.decode())
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const volume = await daytona.volume.get('shared-data', true)
const mountDir = '/home/daytona/volume'
// Producer: write data into the volume, then delete the sandbox
const producer = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
await producer.fs.uploadFile(Buffer.from('shared payload'), `${mountDir}/payload.bin`)
await daytona.delete(producer)
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
const consumer = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
const data = await consumer.fs.downloadFile(`${mountDir}/payload.bin`)
console.log(data.toString())
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.get('shared-data', create: true)
mount_dir = '/home/daytona/volume'
mount = DaytonaApiClient::SandboxVolume.new(volume_id: volume.id, mount_path: mount_dir)
# Producer: write data into the volume, then delete the sandbox
producer = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [mount]
))
producer.fs.upload_file('shared payload', "#{mount_dir}/payload.bin")
daytona.delete(producer)
# Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [mount]
))
data = consumer.fs.download_file("#{mount_dir}/payload.bin")
puts data
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Get(ctx, "shared-data")
if err != nil {
volume, err = client.Volume.Create(ctx, "shared-data")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/volume"
mount := types.VolumeMount{VolumeID: volume.ID, MountPath: mountDir}
// Producer: write data into the volume, then delete the sandbox
producer, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{mount},
},
})
if err != nil {
log.Fatal(err)
}
if err := producer.FileSystem.UploadFile(ctx, []byte("shared payload"), mountDir+"/payload.bin"); err != nil {
log.Fatal(err)
}
if err := producer.Delete(ctx); err != nil {
log.Fatal(err)
}
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{mount},
},
})
if err != nil {
log.Fatal(err)
}
data, err := consumer.FileSystem.DownloadFile(ctx, mountDir+"/payload.bin", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.exception.DaytonaNotFoundException;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.Volume;
import io.daytona.sdk.model.VolumeMount;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume;
try {
volume = daytona.volume().getByName("shared-data");
} catch (DaytonaNotFoundException e) {
volume = daytona.volume().create("shared-data");
}
String mountDir = "/home/daytona/volume";
VolumeMount mount = new VolumeMount();
mount.setVolumeId(volume.getId());
mount.setMountPath(mountDir);
// Producer: write data into the volume, then delete the sandbox
CreateSandboxFromSnapshotParams producerParams = new CreateSandboxFromSnapshotParams();
producerParams.setLanguage("python");
producerParams.setVolumes(Collections.singletonList(mount));
Sandbox producer = daytona.create(producerParams);
producer.fs.uploadFile(
"shared payload".getBytes(StandardCharsets.UTF_8),
mountDir + "/payload.bin");
producer.delete();
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
CreateSandboxFromSnapshotParams consumerParams = new CreateSandboxFromSnapshotParams();
consumerParams.setLanguage("python");
consumerParams.setVolumes(Collections.singletonList(mount));
Sandbox consumer = daytona.create(consumerParams);
byte[] data = consumer.fs.downloadFile(mountDir + "/payload.bin");
System.out.println(new String(data, StandardCharsets.UTF_8));
}
}
}
```
## Mount multiple volumes to one sandbox
Daytona provides an option to mount more than one volume to a single sandbox by passing multiple entries in the `volumes` list. Use this pattern to combine shared assets, models, or datasets in one volume with separate per-application or per-user state in another, exposed at distinct mount paths.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
shared_assets = daytona.volume.get("shared-assets", create=True)
logs = daytona.volume.get("logs", create=True)
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[
VolumeMount(volume_id=shared_assets.id, mount_path="/home/daytona/assets"),
VolumeMount(volume_id=logs.id, mount_path="/home/daytona/logs"),
],
))
```
```typescript
const sharedAssets = await daytona.volume.get('shared-assets', true)
const logs = await daytona.volume.get('logs', true)
const sandbox = await daytona.create({
language: 'typescript',
volumes: [
{ volumeId: sharedAssets.id, mountPath: '/home/daytona/assets' },
{ volumeId: logs.id, mountPath: '/home/daytona/logs' },
],
})
```
```ruby
shared_assets = daytona.volume.get('shared-assets', create: true)
logs = daytona.volume.get('logs', create: true)
params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [
DaytonaApiClient::SandboxVolume.new(volume_id: shared_assets.id, mount_path: '/home/daytona/assets'),
DaytonaApiClient::SandboxVolume.new(volume_id: logs.id, mount_path: '/home/daytona/logs')
]
)
sandbox = daytona.create(params)
```
```go
sharedAssets, err := client.Volume.Get(ctx, "shared-assets")
if err != nil {
sharedAssets, err = client.Volume.Create(ctx, "shared-assets")
if err != nil {
log.Fatal(err)
}
}
logs, err := client.Volume.Get(ctx, "logs")
if err != nil {
logs, err = client.Volume.Create(ctx, "logs")
if err != nil {
log.Fatal(err)
}
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: sharedAssets.ID, MountPath: "/home/daytona/assets"},
{VolumeID: logs.ID, MountPath: "/home/daytona/logs"},
},
},
})
if err != nil {
log.Fatal(err)
}
```
```java
Volume sharedAssets;
try {
sharedAssets = daytona.volume().getByName("shared-assets");
} catch (DaytonaNotFoundException e) {
sharedAssets = daytona.volume().create("shared-assets");
}
Volume logs;
try {
logs = daytona.volume().getByName("logs");
} catch (DaytonaNotFoundException e) {
logs = daytona.volume().create("logs");
}
VolumeMount assetsMount = new VolumeMount();
assetsMount.setVolumeId(sharedAssets.getId());
assetsMount.setMountPath("/home/daytona/assets");
VolumeMount logsMount = new VolumeMount();
logsMount.setVolumeId(logs.getId());
logsMount.setMountPath("/home/daytona/logs");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setLanguage("python");
params.setVolumes(java.util.Arrays.asList(assetsMount, logsMount));
Sandbox sandbox = daytona.create(params);
```
## Multi-tenant isolation with subpaths
Daytona provides an option to isolate per-tenant or per-user data inside a single shared volume by setting a unique `subpath` on each sandbox's volume mount. Each sandbox sees only files under its assigned subpath at `mount_path` and cannot read or write sibling subpaths within the same volume. This is the recommended pattern for multi-tenant workloads because it stays within the [per-organization volume limit](#pricing--limits) instead of creating one volume per tenant.
Isolation is enforced at the FUSE mount boundary. Each sandbox sees its assigned subpath as the volume root, so a sandbox mounted at `users/alice` cannot reach `users/bob` through relative paths such as `../bob`.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("tenants", create=True)
mount_dir = "/home/daytona/data"
# Tenant A
alice_sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/alice")],
))
alice_sandbox.fs.upload_file(b"alice's data", f"{mount_dir}/notes.txt")
# Tenant B sees only its own subpath; alice's notes.txt is invisible
bob_sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/bob")],
))
bob_sandbox.fs.upload_file(b"bob's data", f"{mount_dir}/notes.txt")
```
```typescript
const volume = await daytona.volume.get('tenants', true)
const mountDir = '/home/daytona/data'
// Tenant A
const aliceSandbox = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/alice' }],
})
await aliceSandbox.fs.uploadFile(Buffer.from("alice's data"), `${mountDir}/notes.txt`)
// Tenant B sees only its own subpath; alice's notes.txt is invisible
const bobSandbox = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/bob' }],
})
await bobSandbox.fs.uploadFile(Buffer.from("bob's data"), `${mountDir}/notes.txt`)
```
```ruby
volume = daytona.volume.get('tenants', create: true)
mount_dir = '/home/daytona/data'
# Tenant A
alice_params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id, mount_path: mount_dir, subpath: 'users/alice'
)]
)
alice_sandbox = daytona.create(alice_params)
alice_sandbox.fs.upload_file("alice's data", "#{mount_dir}/notes.txt")
# Tenant B sees only its own subpath; alice's notes.txt is invisible
bob_params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id, mount_path: mount_dir, subpath: 'users/bob'
)]
)
bob_sandbox = daytona.create(bob_params)
bob_sandbox.fs.upload_file("bob's data", "#{mount_dir}/notes.txt")
```
```go
volume, err := client.Volume.Get(ctx, "tenants")
if err != nil {
volume, err = client.Volume.Create(ctx, "tenants")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/data"
// Tenant A
aliceSubpath := "users/alice"
aliceSandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &aliceSubpath},
},
},
})
if err != nil {
log.Fatal(err)
}
if err := aliceSandbox.FileSystem.UploadFile(ctx, []byte("alice's data"), mountDir+"/notes.txt"); err != nil {
log.Fatal(err)
}
// Tenant B sees only its own subpath; alice's notes.txt is invisible
bobSubpath := "users/bob"
bobSandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &bobSubpath},
},
},
})
if err != nil {
log.Fatal(err)
}
if err := bobSandbox.FileSystem.UploadFile(ctx, []byte("bob's data"), mountDir+"/notes.txt"); err != nil {
log.Fatal(err)
}
```
The Java SDK and CLI do not currently expose `subpath`. Use the REST API directly when you need subpath mounts from those clients.
```bash
# Tenant A
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/data",
"subpath": "users/alice"
}
]
}'
# Tenant B
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/data",
"subpath": "users/bob"
}
]
}'
```
## Limitations
Since volumes are FUSE-based mounts, they can not be used for applications that require block storage access (like database tables). Volumes are generally slower for both read and write operations compared to the local sandbox file system.
## Pricing & Limits
Daytona volumes are included at no additional cost. Each organization can create up to 100 volumes, and volume data does not count against your storage quota.
You can view your current volume usage in the [Daytona Volumes ↗](https://app.daytona.io/dashboard/volumes).
# Mount External Storage
Mount object storage (Amazon S3, Cloudflare R2, Google Cloud Storage, Azure Blob) and filesystems like Archil and MesaFS into a Daytona sandbox as a regular directory. The sandbox reads from and writes to the bucket as if it were a local directory, so existing tools, scripts, and agents work without changes. This is useful for bringing in datasets, model weights, or build artifacts that already live in your own cloud account.
External storage mounts and [Daytona Volumes](https://www.daytona.io/docs/en/volumes.md) are complementary FUSE-based mechanisms — both expose remote object storage as a regular sandbox directory, both can be shared across sandboxes, and both persist beyond any individual sandbox's lifetime. The main distinction is **where the data physically lives**: Daytona Volumes are hosted on Daytona's own S3-compatible object store, while external mounts connect to a bucket or filesystem hosted on another provider (Amazon S3, Cloudflare R2, GCS, Azure Blob, Archil, MesaFS).
External storage is mounted using FUSE. Daytona supports two approaches, and each provider section below shows both — pick whichever fits your workflow:
- **Pre-built snapshot** — build a [snapshot](https://www.daytona.io/docs/en/snapshots.md) once with the FUSE tool (`mount-s3`, `gcsfuse`, `blobfuse2`) built-in, then launch every sandbox from that snapshot. Cold starts are fast and predictable. Best for production.
- **Runtime install** — launch a default sandbox and `apt-get install` the FUSE tool when the sandbox starts. Adds time to sandbox startup, but you don't manage snapshots. Best for quick experiments.
Both approaches end with the same mount command and the same usage — the only difference is when the FUSE tool gets installed.
## Mount an Amazon S3 bucket
Mount an S3 bucket using [Mountpoint for Amazon S3 ↗](https://github.com/awslabs/mountpoint-s3) — AWS's official FUSE client, optimized for high throughput on S3.
**Credentials** — set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your local environment. The snippets below pass them into the sandbox via `envVars`, and `mount-s3` reads them from there.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch all S3-enabled sandboxes from that snapshot. This removes per-sandbox package install work, keeps cold starts predictable, and gives you a reusable baseline image for production workloads.
#### Build a snapshot
Create a reusable snapshot that installs `mount-s3` and its system dependencies. After it finishes, every sandbox launched from `fuse-s3` already has the mount binary available.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-s3", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-s3', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-s3', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-s3",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-s3", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass AWS credentials as environment variables on sandbox creation. `mount-s3` reads them automatically.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-s3",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["AWS_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["AWS_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/s3"
# mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"mount-s3 my-bucket {mount_path}")
# Read and write through the mount as if it were a local directory
sandbox.process.exec(f"echo 'hello from Daytona' > {mount_path}/hello.txt")
response = sandbox.process.exec(f"cat {mount_path}/hello.txt")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-s3',
envVars: {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/s3'
// mount-s3 daemonizes by default and reads AWS_* from the environment
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`mount-s3 my-bucket ${mountPath}`)
// Read and write through the mount as if it were a local directory
await sandbox.process.executeCommand(`echo 'hello from Daytona' > ${mountPath}/hello.txt`)
const response = await sandbox.process.executeCommand(`cat ${mountPath}/hello.txt`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-s3',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('AWS_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('AWS_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/s3'
# mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "mount-s3 my-bucket #{mount_path}")
# Read and write through the mount as if it were a local directory
sandbox.process.exec(command: "echo 'hello from Daytona' > #{mount_path}/hello.txt")
response = sandbox.process.exec(command: "cat #{mount_path}/hello.txt")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-s3",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("AWS_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/s3"
// mount-s3 daemonizes by default and reads AWS_* from the environment
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mount-s3 my-bucket "+mountPath); err != nil {
log.Fatal(err)
}
// Read and write through the mount as if it were a local directory
if _, err := sandbox.Process.ExecuteCommand(ctx, "echo 'hello from Daytona' > "+mountPath+"/hello.txt"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "cat "+mountPath+"/hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-s3");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/s3";
// mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand("mount-s3 my-bucket " + mountPath);
// Read and write through the mount as if it were a local directory
sandbox.getProcess().executeCommand(
"echo 'hello from Daytona' > " + mountPath + "/hello.txt");
ExecuteResponse response = sandbox.getProcess().executeCommand(
"cat " + mountPath + "/hello.txt");
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `mount-s3` during startup before running the mount command. This is useful for quick testing and temporary environments where you do not want to maintain a custom snapshot, with the tradeoff of slower cold starts.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["AWS_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["AWS_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3 at runtime
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount and use
mount_path = "/home/daytona/s3"
sandbox.process.exec(f"mkdir -p {mount_path} && mount-s3 my-bucket {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3 at runtime
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount and use
const mountPath = '/home/daytona/s3'
await sandbox.process.executeCommand(`mkdir -p ${mountPath} && mount-s3 my-bucket ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('AWS_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('AWS_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3 at runtime
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount and use
mount_path = '/home/daytona/s3'
sandbox.process.exec(command: "mkdir -p #{mount_path} && mount-s3 my-bucket #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("AWS_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3 at runtime
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount and use
mountPath := "/home/daytona/s3"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && mount-s3 my-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3 at runtime
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount and use
String mountPath = "/home/daytona/s3";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && mount-s3 my-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount a Cloudflare R2 bucket
Cloudflare R2 is S3-compatible, so the same `mount-s3` tool works. Pass an explicit `--endpoint-url` pointing at your R2 account.
**Credentials** — set `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, and `R2_SECRET_ACCESS_KEY` in your local environment. R2 is S3-compatible, so the snippets below pass your R2 keys into the sandbox via `envVars` under the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` names that `mount-s3` expects.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch all R2-enabled sandboxes from that snapshot. The mount flow stays identical to S3 except for the R2 `--endpoint-url`, and startup remains fast because installation is done once at snapshot build time.
#### Build a snapshot
Create a reusable snapshot that installs the same `mount-s3` tool used for S3. R2 remains S3-compatible, so this snapshot is identical to S3 setup and only the runtime mount command changes.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-r2", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-r2', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-r2', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-r2",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-r2", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass your R2 credentials into the sandbox as `AWS_*` environment variables and mount with the R2 endpoint URL. This keeps the authentication flow compatible with `mount-s3` while targeting your Cloudflare account.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
account_id = os.environ["R2_ACCOUNT_ID"]
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-r2",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["R2_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["R2_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/r2"
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(
f"mount-s3 --endpoint-url https://{account_id}.r2.cloudflarestorage.com "
f"my-r2-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
const accountId = process.env.R2_ACCOUNT_ID!
const sandbox = await daytona.create({
snapshot: 'fuse-r2',
envVars: {
AWS_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/r2'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://${accountId}.r2.cloudflarestorage.com ` +
`my-r2-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
account_id = ENV.fetch('R2_ACCOUNT_ID')
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-r2',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('R2_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('R2_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/r2'
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(
command: "mount-s3 --endpoint-url https://#{account_id}.r2.cloudflarestorage.com " \
"my-r2-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
accountID := os.Getenv("R2_ACCOUNT_ID")
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-r2",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("R2_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/r2"
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mount-s3 --endpoint-url https://"+accountID+".r2.cloudflarestorage.com "+
"my-r2-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
String accountId = System.getenv("R2_ACCOUNT_ID");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-r2");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("R2_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/r2";
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"mount-s3 --endpoint-url https://" + accountId + ".r2.cloudflarestorage.com "
+ "my-r2-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `mount-s3` during startup, then mount your bucket with the R2 `--endpoint-url`. This path is convenient for prototyping or one-off tasks, but each new sandbox pays the package installation cost.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
account_id = os.environ["R2_ACCOUNT_ID"]
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["R2_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["R2_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount with R2 endpoint
mount_path = "/home/daytona/r2"
sandbox.process.exec(
f"mkdir -p {mount_path} && "
f"mount-s3 --endpoint-url https://{account_id}.r2.cloudflarestorage.com "
f"my-r2-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const accountId = process.env.R2_ACCOUNT_ID!
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount with R2 endpoint
const mountPath = '/home/daytona/r2'
await sandbox.process.executeCommand(
`mkdir -p ${mountPath} && ` +
`mount-s3 --endpoint-url https://${accountId}.r2.cloudflarestorage.com ` +
`my-r2-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
account_id = ENV.fetch('R2_ACCOUNT_ID')
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('R2_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('R2_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount with R2 endpoint
mount_path = '/home/daytona/r2'
sandbox.process.exec(
command: "mkdir -p #{mount_path} && " \
"mount-s3 --endpoint-url https://#{account_id}.r2.cloudflarestorage.com " \
"my-r2-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
accountID := os.Getenv("R2_ACCOUNT_ID")
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("R2_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount with R2 endpoint
mountPath := "/home/daytona/r2"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && "+
"mount-s3 --endpoint-url https://"+accountID+".r2.cloudflarestorage.com "+
"my-r2-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String accountId = System.getenv("R2_ACCOUNT_ID");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("R2_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount with R2 endpoint
String mountPath = "/home/daytona/r2";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && "
+ "mount-s3 --endpoint-url https://" + accountId + ".r2.cloudflarestorage.com "
+ "my-r2-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount a Tigris bucket
Mount a Tigris bucket with the same `mount-s3` tool used for S3. Pass `--endpoint-url https://t3.storage.dev`, because Tigris uses one global endpoint with no per-account subdomain. Tigris also supports bucket snapshots and copy-on-write forks through request headers, so each sandbox can use an isolated writable bucket without duplicating source data.
**Credentials** — set `TIGRIS_STORAGE_ACCESS_KEY_ID` and `TIGRIS_STORAGE_SECRET_ACCESS_KEY` in your local environment. The snippets below pass these into the sandbox via `envVars` under the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` names that `mount-s3` expects.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch Tigris sandboxes from that snapshot. The mount flow matches S3 except for the Tigris `--endpoint-url`, and startup stays fast because installation happens once during snapshot build.
#### Build a snapshot
Create a reusable snapshot that installs `mount-s3`. Because Tigris is S3-compatible, this setup matches S3 and only the runtime mount command changes.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-tigris", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-tigris', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-tigris', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-tigris",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-tigris", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass Tigris credentials into the sandbox as `AWS_*` environment variables, then mount with the Tigris endpoint URL. This keeps authentication compatible with `mount-s3` while targeting your Tigris account.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-tigris",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["TIGRIS_STORAGE_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["TIGRIS_STORAGE_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/tigris"
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(
f"mount-s3 --endpoint-url https://t3.storage.dev "
f"my-tigris-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-tigris',
envVars: {
AWS_ACCESS_KEY_ID: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://t3.storage.dev ` +
`my-tigris-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-tigris',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('TIGRIS_STORAGE_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('TIGRIS_STORAGE_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/tigris'
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(
command: 'mount-s3 --endpoint-url https://t3.storage.dev ' \
"my-tigris-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-tigris",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/tigris"
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mount-s3 --endpoint-url https://t3.storage.dev "+
"my-tigris-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-tigris");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/tigris";
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"mount-s3 --endpoint-url https://t3.storage.dev "
+ "my-tigris-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox, install `mount-s3` during startup, then mount with the Tigris `--endpoint-url`. This path is convenient for prototyping or one-off tasks, but each new sandbox repeats package installation.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["TIGRIS_STORAGE_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["TIGRIS_STORAGE_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount with Tigris endpoint
mount_path = "/home/daytona/tigris"
sandbox.process.exec(
f"mkdir -p {mount_path} && "
f"mount-s3 --endpoint-url https://t3.storage.dev "
f"my-tigris-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount with Tigris endpoint
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(
`mkdir -p ${mountPath} && ` +
`mount-s3 --endpoint-url https://t3.storage.dev ` +
`my-tigris-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('TIGRIS_STORAGE_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('TIGRIS_STORAGE_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount with Tigris endpoint
mount_path = '/home/daytona/tigris'
sandbox.process.exec(
command: "mkdir -p #{mount_path} && " \
'mount-s3 --endpoint-url https://t3.storage.dev ' \
"my-tigris-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount with Tigris endpoint
mountPath := "/home/daytona/tigris"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && "+
"mount-s3 --endpoint-url https://t3.storage.dev "+
"my-tigris-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxBaseParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxBaseParams params = new CreateSandboxBaseParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount with Tigris endpoint
String mountPath = "/home/daytona/tigris";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && "
+ "mount-s3 --endpoint-url https://t3.storage.dev "
+ "my-tigris-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Mount a copy-on-write fork per sandbox
A Tigris bucket fork is a new bucket created from a snapshot of a source bucket. The fork shares underlying storage with the source until written to — new writes go only to the fork, and the source bucket and other forks are unaffected. Fork creation is constant-time regardless of source bucket size.
This pattern fits Daytona sandboxes when each sandbox needs a writable copy of a shared dataset (model weights, fixtures, golden data) without duplicating it on every launch.
**Prerequisite** — the source bucket must be created with snapshots enabled. This is a one-time setup, done outside the per-sandbox flow:
```typescript
import { createBucket } from '@tigrisdata/storage'
await createBucket('my-source-bucket', { enableSnapshot: true })
```
In any S3 SDK, send a `CreateBucket` request with the header `X-Tigris-Enable-Snapshot: true`.
The `@tigrisdata/agent-kit` package wraps this workflow as `createForks()`. It snapshots the source bucket and creates one or more forks in a single call. Passing `credentials: { role: 'Editor' }` also creates a scoped access key per fork, so each sandbox can read and write only its own fork bucket instead of the full account.
```typescript
import { createForks, teardownForks } from '@tigrisdata/agent-kit'
import { Daytona } from '@daytona/sdk'
const SOURCE_BUCKET = 'my-source-bucket'
// 1. Snapshot the source and create a fork with a scoped access key
const { data: forkSet, error } = await createForks(SOURCE_BUCKET, 1, {
credentials: { role: 'Editor' },
})
if (error) throw error
const fork = forkSet.forks[0]
// 2. Launch the sandbox with the fork's scoped credentials
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-tigris',
envVars: {
AWS_ACCESS_KEY_ID: fork.credentials!.accessKeyId,
AWS_SECRET_ACCESS_KEY: fork.credentials!.secretAccessKey,
},
})
// 3. Mount the fork bucket
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://t3.storage.dev ${fork.bucket} ${mountPath}`,
)
try {
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
} finally {
await daytona.delete(sandbox)
await teardownForks(forkSet) // revokes the scoped key and deletes the fork bucket
}
```
To run the same workflow from a language without an `agent-kit` equivalent, use any S3 SDK and send the headers documented below. Send `X-Tigris-Snapshot: true` on `CreateBucket` for the source name, then capture `X-Tigris-Snapshot-Version` from the response. Next, send `CreateBucket` for the fork name with `X-Tigris-Fork-Source-Bucket` and `X-Tigris-Fork-Source-Bucket-Snapshot`. Mount the fork with the same `mount-s3` snippets shown above, replacing the bucket name with the fork bucket.
#### Reference: Tigris-specific headers
These headers drive snapshot and fork operations over the S3 API. Use them with any AWS SDK (boto3, aws-sdk-go-v2, aws-sdk-java-v2, aws-sdk-ruby) by attaching a request interceptor.
| Header | Sent on | Purpose |
| ------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------ |
| **`X-Tigris-Enable-Snapshot: true`** | **`CreateBucket`** (new source) | Enable snapshots on a new bucket. Required before snapshotting it. |
| **`X-Tigris-Snapshot: true`** | **`CreateBucket`** (existing source name) | Take a snapshot of the bucket. Optional **`; name=`** suffix labels it. |
| **`X-Tigris-Snapshot-Version`** | Response to snapshot create | Snapshot version ID returned to the caller — pass to fork creation. |
| **`X-Tigris-Fork-Source-Bucket`** | **`CreateBucket`** (new fork name) | Source bucket to fork from. |
| **`X-Tigris-Fork-Source-Bucket-Snapshot`** | **`CreateBucket`** (new fork name) | Source snapshot version to fork from. |
Source: [**`tigrisdata/storage`**](https://github.com/tigrisdata/storage). See **`shared/headers.ts`** for the full header set.
## Mount a Google Cloud Storage bucket
Mount a GCS bucket using [gcsfuse ↗](https://github.com/GoogleCloudPlatform/gcsfuse) — Google's official FUSE client.
**Credentials** — `gcsfuse` reads a service account JSON key file. The snippets below read the key from a local path on your host and upload it into the sandbox via `sandbox.fs`.
:::note
Daytona's base image is Debian Trixie, but Google has not published a Trixie-specific gcsfuse repository yet. The `gcsfuse-bookworm` repo packages run cleanly on Trixie, so we use them in the snippets below.
:::
### Pre-built snapshot
Build a snapshot with `gcsfuse` preinstalled, then launch all GCS-enabled sandboxes from that snapshot. This avoids repeating apt repository setup and package installation for every sandbox, which makes startup behavior more consistent.
#### Build a snapshot
Create a reusable snapshot that installs `gcsfuse` plus its apt repository configuration. After this step, GCS-enabled sandboxes can mount immediately without repeating package setup.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg",
"sudo mkdir -p /etc/apt/keyrings "
"&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
"| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg",
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] '
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" '
"| sudo tee /etc/apt/sources.list.d/gcsfuse.list",
"sudo apt-get update && sudo apt-get install -y gcsfuse",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-gcs", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
'sudo mkdir -p /etc/apt/keyrings ' +
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' +
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' +
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list',
'sudo apt-get update && sudo apt-get install -y gcsfuse',
)
await daytona.snapshot.create(
{ name: 'fuse-gcs', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
'sudo mkdir -p /etc/apt/keyrings ' \
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' \
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' \
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list',
'sudo apt-get update && sudo apt-get install -y gcsfuse'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-gcs', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg").
Run("sudo mkdir -p /etc/apt/keyrings && " +
"curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | " +
"sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg").
Run(`echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ` +
`https://packages.cloud.google.com/apt gcsfuse-bookworm main" | ` +
`sudo tee /etc/apt/sources.list.d/gcsfuse.list`).
Run("sudo apt-get update && sudo apt-get install -y gcsfuse")
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-gcs",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg",
"sudo mkdir -p /etc/apt/keyrings "
+ "&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
+ "| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg",
"echo \"deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] "
+ "https://packages.cloud.google.com/apt gcsfuse-bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/gcsfuse.list",
"sudo apt-get update && sudo apt-get install -y gcsfuse"
);
daytona.snapshot().create("fuse-gcs", image, System.out::println);
}
}
}
```
#### Launch and mount
`gcsfuse` authenticates to GCS with a service account JSON key. Upload it into the sandbox via `sandbox.fs` and point `gcsfuse` at it with `--key-file`.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = os.environ["GCS_SERVICE_ACCOUNT_KEY"].encode()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="fuse-gcs"))
mount_path = "/home/daytona/gcs"
key_path = "/home/daytona/.gcs-key.json"
# Upload the key file into the sandbox
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(f"chmod 600 {key_path}")
# Mount the bucket
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"gcsfuse --key-file={key_path} my-gcs-bucket {mount_path}")
# Use the mount
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
const serviceAccountKey = Buffer.from(process.env.GCS_SERVICE_ACCOUNT_KEY!)
const sandbox = await daytona.create({ snapshot: 'fuse-gcs' })
const mountPath = '/home/daytona/gcs'
const keyPath = '/home/daytona/.gcs-key.json'
// Upload the key file into the sandbox
await sandbox.fs.uploadFile(serviceAccountKey, keyPath)
await sandbox.process.executeCommand(`chmod 600 ${keyPath}`)
// Mount the bucket
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`gcsfuse --key-file=${keyPath} my-gcs-bucket ${mountPath}`)
// Use the mount
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = ENV.fetch('GCS_SERVICE_ACCOUNT_KEY')
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'fuse-gcs')
)
mount_path = '/home/daytona/gcs'
key_path = '/home/daytona/.gcs-key.json'
# Upload the key file into the sandbox
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(command: "chmod 600 #{key_path}")
# Mount the bucket
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "gcsfuse --key-file=#{key_path} my-gcs-bucket #{mount_path}")
# Use the mount
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
serviceAccountKey := []byte(os.Getenv("GCS_SERVICE_ACCOUNT_KEY"))
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-gcs",
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/gcs"
keyPath := "/home/daytona/.gcs-key.json"
// Upload the key file into the sandbox
if err := sandbox.FileSystem.UploadFile(ctx, serviceAccountKey, keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+keyPath); err != nil {
log.Fatal(err)
}
// Mount the bucket
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"gcsfuse --key-file="+keyPath+" my-gcs-bucket "+mountPath); err != nil {
log.Fatal(err)
}
// Use the mount
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
byte[] serviceAccountKey = System.getenv("GCS_SERVICE_ACCOUNT_KEY")
.getBytes(StandardCharsets.UTF_8);
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-gcs");
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/gcs";
String keyPath = "/home/daytona/.gcs-key.json";
// Upload the key file into the sandbox
sandbox.fs.uploadFile(serviceAccountKey, keyPath);
sandbox.getProcess().executeCommand("chmod 600 " + keyPath);
// Mount the bucket
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"gcsfuse --key-file=" + keyPath + " my-gcs-bucket " + mountPath);
// Use the mount
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `gcsfuse` when the sandbox starts, then upload the service account key and mount the bucket. This is the fastest way to iterate on setup, but every sandbox repeats install and key staging steps.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = os.environ["GCS_SERVICE_ACCOUNT_KEY"].encode()
sandbox = daytona.create(CreateSandboxBaseParams())
# Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg"
)
sandbox.process.exec(
"sudo mkdir -p /etc/apt/keyrings "
"&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
"| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg"
)
sandbox.process.exec(
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] '
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" '
"| sudo tee /etc/apt/sources.list.d/gcsfuse.list "
"&& sudo apt-get update && sudo apt-get install -y gcsfuse"
)
# Upload the key and mount
mount_path = "/home/daytona/gcs"
key_path = "/home/daytona/.gcs-key.json"
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(f"chmod 600 {key_path}")
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"gcsfuse --key-file={key_path} my-gcs-bucket {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
const serviceAccountKey = Buffer.from(process.env.GCS_SERVICE_ACCOUNT_KEY!)
const sandbox = await daytona.create()
// Install gcsfuse from the bookworm repo (works on Trixie)
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
)
await sandbox.process.executeCommand(
'sudo mkdir -p /etc/apt/keyrings ' +
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' +
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
)
await sandbox.process.executeCommand(
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' +
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list ' +
'&& sudo apt-get update && sudo apt-get install -y gcsfuse',
)
// Upload the key and mount
const mountPath = '/home/daytona/gcs'
const keyPath = '/home/daytona/.gcs-key.json'
await sandbox.fs.uploadFile(serviceAccountKey, keyPath)
await sandbox.process.executeCommand(`chmod 600 ${keyPath}`)
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`gcsfuse --key-file=${keyPath} my-gcs-bucket ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = ENV.fetch('GCS_SERVICE_ACCOUNT_KEY')
sandbox = daytona.create(Daytona::CreateSandboxBaseParams.new)
# Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg'
)
sandbox.process.exec(
command: 'sudo mkdir -p /etc/apt/keyrings ' \
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' \
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg'
)
sandbox.process.exec(
command: 'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' \
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list ' \
'&& sudo apt-get update && sudo apt-get install -y gcsfuse'
)
# Upload the key and mount
mount_path = '/home/daytona/gcs'
key_path = '/home/daytona/.gcs-key.json'
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(command: "chmod 600 #{key_path}")
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "gcsfuse --key-file=#{key_path} my-gcs-bucket #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
serviceAccountKey := []byte(os.Getenv("GCS_SERVICE_ACCOUNT_KEY"))
sandbox, err := client.Create(ctx, types.SnapshotParams{})
if err != nil {
log.Fatal(err)
}
// Install gcsfuse from the bookworm repo (works on Trixie)
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo mkdir -p /etc/apt/keyrings && "+
"curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | "+
"sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] `+
`https://packages.cloud.google.com/apt gcsfuse-bookworm main" | `+
`sudo tee /etc/apt/sources.list.d/gcsfuse.list && `+
`sudo apt-get update && sudo apt-get install -y gcsfuse`); err != nil {
log.Fatal(err)
}
// Upload the key and mount
mountPath := "/home/daytona/gcs"
keyPath := "/home/daytona/.gcs-key.json"
if err := sandbox.FileSystem.UploadFile(ctx, serviceAccountKey, keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"gcsfuse --key-file="+keyPath+" my-gcs-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
byte[] serviceAccountKey = System.getenv("GCS_SERVICE_ACCOUNT_KEY")
.getBytes(StandardCharsets.UTF_8);
Sandbox sandbox = daytona.create(new CreateSandboxFromSnapshotParams());
// Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg");
sandbox.getProcess().executeCommand(
"sudo mkdir -p /etc/apt/keyrings "
+ "&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
+ "| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg");
sandbox.getProcess().executeCommand(
"echo \"deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] "
+ "https://packages.cloud.google.com/apt gcsfuse-bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/gcsfuse.list "
+ "&& sudo apt-get update && sudo apt-get install -y gcsfuse");
// Upload the key and mount
String mountPath = "/home/daytona/gcs";
String keyPath = "/home/daytona/.gcs-key.json";
sandbox.fs.uploadFile(serviceAccountKey, keyPath);
sandbox.getProcess().executeCommand("chmod 600 " + keyPath);
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"gcsfuse --key-file=" + keyPath + " my-gcs-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount an Azure Blob container
Mount an Azure Blob container using [blobfuse2 ↗](https://github.com/Azure/azure-storage-fuse) — Microsoft's official FUSE client.
**Credentials** — set `AZURE_STORAGE_ACCOUNT`, `AZURE_STORAGE_CONTAINER`, and `AZURE_STORAGE_ACCOUNT_KEY` in your local environment. The snippets below pass them into the sandbox via `envVars`, and `blobfuse2` reads them from its YAML config.
:::caution
Daytona's base image is Debian Trixie. Microsoft's `blobfuse2` Bookworm binary links against `libfuse3.so.3`, but Trixie's `fuse3` package bumped the SONAME to `.4`, so `libfuse3.so.3` is missing on disk. The snapshot recipe below creates a compatibility symlink. Without it, `blobfuse2` fails with `libfuse3.so.3: cannot open shared object file`.
:::
### Pre-built snapshot
Build a snapshot with `blobfuse2` and required FUSE compatibility setup preinstalled, then launch all Azure-enabled sandboxes from that snapshot. This is the recommended path for stable environments because dependency and compatibility work runs once during snapshot creation.
#### Build a snapshot
Create a reusable snapshot that installs `blobfuse2`, configures required FUSE dependencies, and applies the Trixie compatibility steps. This ensures Azure mounts work out of the box in sandboxes launched from `fuse-azure`.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget",
# Microsoft's apt repo (use bookworm packages on Trixie)
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
"| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg",
'echo "deb [arch=$(dpkg --print-architecture) '
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] '
'https://packages.microsoft.com/debian/12/prod bookworm main" '
"| sudo tee /etc/apt/sources.list.d/microsoft-prod.list",
"sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3",
# libfuse3.so.3 compat symlink for Trixie (see :::caution above)
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null '
"| sort -V | tail -1) "
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" '
"&& sudo ldconfig",
"sudo touch /etc/fuse.conf "
'&& grep -qxF "user_allow_other" /etc/fuse.conf '
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf',
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-azure", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
// Microsoft's apt repo (use bookworm packages on Trixie)
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' +
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
'echo "deb [arch=$(dpkg --print-architecture) ' +
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' +
'https://packages.microsoft.com/debian/12/prod bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list',
'sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
// libfuse3.so.3 compat symlink for Trixie (see :::caution above)
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' +
'| sort -V | tail -1) ' +
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' +
'&& sudo ldconfig',
'sudo touch /etc/fuse.conf ' +
'&& grep -qxF "user_allow_other" /etc/fuse.conf ' +
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf',
)
await daytona.snapshot.create(
{ name: 'fuse-azure', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
# Microsoft's apt repo (use bookworm packages on Trixie)
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' \
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
'echo "deb [arch=$(dpkg --print-architecture) ' \
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' \
'https://packages.microsoft.com/debian/12/prod bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list',
'sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
# libfuse3.so.3 compat symlink for Trixie
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' \
'| sort -V | tail -1) ' \
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' \
'&& sudo ldconfig',
'sudo touch /etc/fuse.conf ' \
'&& grep -qxF "user_allow_other" /etc/fuse.conf ' \
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-azure', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget").
// Microsoft's apt repo (use bookworm packages on Trixie)
Run("wget -qO- https://packages.microsoft.com/keys/microsoft.asc | " +
"sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg").
Run(`echo "deb [arch=$(dpkg --print-architecture) ` +
`signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ` +
`https://packages.microsoft.com/debian/12/prod bookworm main" | ` +
`sudo tee /etc/apt/sources.list.d/microsoft-prod.list`).
Run("sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3").
// libfuse3.so.3 compat symlink for Trixie
Run(`src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null | sort -V | tail -1) && ` +
`sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" && sudo ldconfig`).
Run(`sudo touch /etc/fuse.conf && grep -qxF "user_allow_other" /etc/fuse.conf || ` +
`echo "user_allow_other" | sudo tee -a /etc/fuse.conf`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-azure",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget",
// Microsoft's apt repo (use bookworm packages on Trixie)
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
+ "| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg",
"echo \"deb [arch=$(dpkg --print-architecture) "
+ "signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] "
+ "https://packages.microsoft.com/debian/12/prod bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/microsoft-prod.list",
"sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3",
// libfuse3.so.3 compat symlink for Trixie
"src=$(find /usr/lib /lib -name \"libfuse3.so.3.*\" -type f 2>/dev/null "
+ "| sort -V | tail -1) "
+ "&& sudo ln -sfn \"$src\" \"$(dirname \"$src\")/libfuse3.so.3\" "
+ "&& sudo ldconfig",
"sudo touch /etc/fuse.conf "
+ "&& grep -qxF \"user_allow_other\" /etc/fuse.conf "
+ "|| echo \"user_allow_other\" | sudo tee -a /etc/fuse.conf"
);
daytona.snapshot().create("fuse-azure", image, System.out::println);
}
}
}
```
#### Launch and mount
`blobfuse2` reads its configuration from a YAML file. Build it with your account credentials and upload it into the sandbox.
The YAML below tells `blobfuse2` three things: **where to connect** (the `azstorage:` block — your storage account, the container you want to mount, the endpoint URL, and the auth method), **what to enable** (the `components:` list — the FUSE interface itself, a content cache, a metadata cache, and the Azure backend), and **how to log**. The cache components use sensible defaults when listed without their own top-level config blocks; add explicit `block_cache:` / `attr_cache:` blocks later if you need to tune cache sizes or timeouts. Note that in Azure terminology, a "container" is the equivalent of an S3 bucket — it's specified inside the YAML rather than passed as a command-line argument.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="fuse-azure"))
mount_path = "/home/daytona/azure"
config_path = "/home/daytona/.blobfuse2.yaml"
account = os.environ["AZURE_STORAGE_ACCOUNT"]
container = os.environ["AZURE_STORAGE_CONTAINER"]
account_key = os.environ["AZURE_STORAGE_ACCOUNT_KEY"]
config = f"""\
allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: {account}
container: {container}
endpoint: https://{account}.blob.core.windows.net
auth-type: key
account-key: {account_key}
"""
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec(f"chmod 600 {config_path}")
# Mount the container
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"blobfuse2 mount --config-file={config_path} {mount_path}")
# Use the mount
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({ snapshot: 'fuse-azure' })
const mountPath = '/home/daytona/azure'
const configPath = '/home/daytona/.blobfuse2.yaml'
const account = process.env.AZURE_STORAGE_ACCOUNT!
const container = process.env.AZURE_STORAGE_CONTAINER!
const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY!
const config = `allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: ${account}
container: ${container}
endpoint: https://${account}.blob.core.windows.net
auth-type: key
account-key: ${accountKey}
`
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand(`chmod 600 ${configPath}`)
// Mount the container
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`blobfuse2 mount --config-file=${configPath} ${mountPath}`)
// Use the mount
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'fuse-azure')
)
mount_path = '/home/daytona/azure'
config_path = '/home/daytona/.blobfuse2.yaml'
account = ENV.fetch('AZURE_STORAGE_ACCOUNT')
container = ENV.fetch('AZURE_STORAGE_CONTAINER')
account_key = ENV.fetch('AZURE_STORAGE_ACCOUNT_KEY')
config = <<~YAML
allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: #{account}
container: #{container}
endpoint: https://#{account}.blob.core.windows.net
auth-type: key
account-key: #{account_key}
YAML
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: "chmod 600 #{config_path}")
# Mount the container
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "blobfuse2 mount --config-file=#{config_path} #{mount_path}")
# Use the mount
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-azure",
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/azure"
configPath := "/home/daytona/.blobfuse2.yaml"
account := os.Getenv("AZURE_STORAGE_ACCOUNT")
container := os.Getenv("AZURE_STORAGE_CONTAINER")
accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
config := fmt.Sprintf(`allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: %s
container: %s
endpoint: https://%s.blob.core.windows.net
auth-type: key
account-key: %s
`, account, container, account, accountKey)
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+configPath); err != nil {
log.Fatal(err)
}
// Mount the container
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"blobfuse2 mount --config-file="+configPath+" "+mountPath); err != nil {
log.Fatal(err)
}
// Use the mount
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-azure");
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/azure";
String configPath = "/home/daytona/.blobfuse2.yaml";
String account = System.getenv("AZURE_STORAGE_ACCOUNT");
String container = System.getenv("AZURE_STORAGE_CONTAINER");
String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY");
String config = "allow-other: true\n"
+ "logging:\n"
+ " type: syslog\n"
+ " level: log_warning\n"
+ "components:\n"
+ " - libfuse\n"
+ " - block_cache\n"
+ " - attr_cache\n"
+ " - azstorage\n"
+ "azstorage:\n"
+ " type: block\n"
+ " account-name: " + account + "\n"
+ " container: " + container + "\n"
+ " endpoint: https://" + account + ".blob.core.windows.net\n"
+ " auth-type: key\n"
+ " account-key: " + accountKey + "\n";
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand("chmod 600 " + configPath);
// Mount the container
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"blobfuse2 mount --config-file=" + configPath + " " + mountPath);
// Use the mount
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `blobfuse2` during startup before writing the config and mounting the container. This is useful for quick validation and experiments, with the tradeoff of slower cold starts and repeated setup on each sandbox launch.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(CreateSandboxBaseParams())
# Install blobfuse2
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget"
)
sandbox.process.exec(
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
"| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg"
)
sandbox.process.exec(
'echo "deb [arch=$(dpkg --print-architecture) '
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] '
'https://packages.microsoft.com/debian/12/prod bookworm main" '
"| sudo tee /etc/apt/sources.list.d/microsoft-prod.list "
"&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3"
)
# libfuse3.so.3 compat symlink for Trixie
sandbox.process.exec(
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null '
"| sort -V | tail -1) "
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" '
"&& sudo ldconfig"
)
# Build config and mount
mount_path = "/home/daytona/azure"
config_path = "/home/daytona/.blobfuse2.yaml"
account = os.environ["AZURE_STORAGE_ACCOUNT"]
container = os.environ["AZURE_STORAGE_CONTAINER"]
account_key = os.environ["AZURE_STORAGE_ACCOUNT_KEY"]
config = f"""\
allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: {account}
container: {container}
endpoint: https://{account}.blob.core.windows.net
auth-type: key
account-key: {account_key}
"""
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec(f"chmod 600 {config_path}")
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"blobfuse2 mount --config-file={config_path} {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create()
// Install blobfuse2
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
)
await sandbox.process.executeCommand(
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' +
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
)
await sandbox.process.executeCommand(
'echo "deb [arch=$(dpkg --print-architecture) ' +
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' +
'https://packages.microsoft.com/debian/12/prod bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list ' +
'&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
)
// libfuse3.so.3 compat symlink for Trixie
await sandbox.process.executeCommand(
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' +
'| sort -V | tail -1) ' +
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' +
'&& sudo ldconfig',
)
// Build config and mount
const mountPath = '/home/daytona/azure'
const configPath = '/home/daytona/.blobfuse2.yaml'
const account = process.env.AZURE_STORAGE_ACCOUNT!
const container = process.env.AZURE_STORAGE_CONTAINER!
const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY!
const config = `allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: ${account}
container: ${container}
endpoint: https://${account}.blob.core.windows.net
auth-type: key
account-key: ${accountKey}
`
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand(`chmod 600 ${configPath}`)
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`blobfuse2 mount --config-file=${configPath} ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(Daytona::CreateSandboxBaseParams.new)
# Install blobfuse2
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget'
)
sandbox.process.exec(
command: 'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' \
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg'
)
sandbox.process.exec(
command: 'echo "deb [arch=$(dpkg --print-architecture) ' \
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' \
'https://packages.microsoft.com/debian/12/prod bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list ' \
'&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3'
)
# libfuse3.so.3 compat symlink for Trixie
sandbox.process.exec(
command: 'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' \
'| sort -V | tail -1) ' \
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' \
'&& sudo ldconfig'
)
# Build config and mount
mount_path = '/home/daytona/azure'
config_path = '/home/daytona/.blobfuse2.yaml'
account = ENV.fetch('AZURE_STORAGE_ACCOUNT')
container = ENV.fetch('AZURE_STORAGE_CONTAINER')
account_key = ENV.fetch('AZURE_STORAGE_ACCOUNT_KEY')
config = <<~YAML
allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: #{account}
container: #{container}
endpoint: https://#{account}.blob.core.windows.net
auth-type: key
account-key: #{account_key}
YAML
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: "chmod 600 #{config_path}")
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "blobfuse2 mount --config-file=#{config_path} #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{})
if err != nil {
log.Fatal(err)
}
// Install blobfuse2
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc | "+
"sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`echo "deb [arch=$(dpkg --print-architecture) `+
`signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] `+
`https://packages.microsoft.com/debian/12/prod bookworm main" | `+
`sudo tee /etc/apt/sources.list.d/microsoft-prod.list && `+
`sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3`); err != nil {
log.Fatal(err)
}
// libfuse3.so.3 compat symlink for Trixie
if _, err := sandbox.Process.ExecuteCommand(ctx,
`src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null | sort -V | tail -1) && `+
`sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" && sudo ldconfig`); err != nil {
log.Fatal(err)
}
// Build config and mount
mountPath := "/home/daytona/azure"
configPath := "/home/daytona/.blobfuse2.yaml"
account := os.Getenv("AZURE_STORAGE_ACCOUNT")
container := os.Getenv("AZURE_STORAGE_CONTAINER")
accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
config := fmt.Sprintf(`allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: %s
container: %s
endpoint: https://%s.blob.core.windows.net
auth-type: key
account-key: %s
`, account, container, account, accountKey)
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"blobfuse2 mount --config-file="+configPath+" "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create(new CreateSandboxFromSnapshotParams());
// Install blobfuse2
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget");
sandbox.getProcess().executeCommand(
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
+ "| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg");
sandbox.getProcess().executeCommand(
"echo \"deb [arch=$(dpkg --print-architecture) "
+ "signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] "
+ "https://packages.microsoft.com/debian/12/prod bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/microsoft-prod.list "
+ "&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3");
// libfuse3.so.3 compat symlink for Trixie
sandbox.getProcess().executeCommand(
"src=$(find /usr/lib /lib -name \"libfuse3.so.3.*\" -type f 2>/dev/null "
+ "| sort -V | tail -1) "
+ "&& sudo ln -sfn \"$src\" \"$(dirname \"$src\")/libfuse3.so.3\" "
+ "&& sudo ldconfig");
// Build config and mount
String mountPath = "/home/daytona/azure";
String configPath = "/home/daytona/.blobfuse2.yaml";
String account = System.getenv("AZURE_STORAGE_ACCOUNT");
String container = System.getenv("AZURE_STORAGE_CONTAINER");
String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY");
String config = "allow-other: true\n"
+ "components:\n"
+ " - libfuse\n"
+ " - block_cache\n"
+ " - attr_cache\n"
+ " - azstorage\n"
+ "azstorage:\n"
+ " type: block\n"
+ " account-name: " + account + "\n"
+ " container: " + container + "\n"
+ " endpoint: https://" + account + ".blob.core.windows.net\n"
+ " auth-type: key\n"
+ " account-key: " + accountKey + "\n";
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand("chmod 600 " + configPath);
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"blobfuse2 mount --config-file=" + configPath + " " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount an Archil disk
[Archil ↗](https://archil.com) is an infinite, elastic, POSIX file system that automatically synchronizes to object storage like S3, R2, and Azure Blob. You should use Archil when you need to mount a bucket to your Daytona sandbox, but need higher out-of-the-box performance than traditional FUSE mounts. Archil achieves higher performance by using shared SSD read and write caching in front of your object storage bucket. Archil disks are mounted as regular directories, scale to whatever your sandbox writes (you pay only for what you use), and can be [mounted by many sandboxes at once ↗](https://docs.archil.com/concepts/shared-disks), making them a natural fit for parallel agents that share state.
**Credentials** — set `ARCHIL_MOUNT_TOKEN` (a disk-scoped [mount token ↗](https://docs.archil.com/concepts/disk-users#disk-token-authorization) generated from the disk's **Details** page in the [Archil console ↗](https://console.archil.com)), `ARCHIL_REGION` (the disk's region, e.g. `aws-us-east-1`), and `ARCHIL_DISK` (the owner-qualified disk name, e.g. `myorg/my-disk`, or disk ID like `dsk-0123456789abcdef`) in your local environment. The snippets below pass them into the sandbox via `envVars`, and the `archil` CLI reads them from there.
### Pre-built snapshot
Build a snapshot with the `archil` CLI preinstalled, then launch Archil-enabled sandboxes from that snapshot. You still authenticate and mount at runtime, but installation is no longer part of each sandbox startup sequence.
#### Build a snapshot
Create a reusable snapshot that installs the `archil` CLI. The `daytonaio/sandbox` base image ships Debian Trixie which doesn't include `libfuse2`, so we apt-install it first; the Archil installer reads the `.deb` it downloads and links against `libfuse2`.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates",
"curl -fsSL https://archil.com/install | sh",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-archil", image=image),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
'curl -fsSL https://archil.com/install | sh',
)
await daytona.snapshot.create(
{ name: 'fuse-archil', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
'curl -fsSL https://archil.com/install | sh'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-archil', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates").
Run("curl -fsSL https://archil.com/install | sh")
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-archil",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates",
"curl -fsSL https://archil.com/install | sh"
);
daytona.snapshot().create("fuse-archil", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass `ARCHIL_MOUNT_TOKEN`, `ARCHIL_REGION`, and `ARCHIL_DISK` to the sandbox via `envVars`. Your code then mounts the disk at `/home/daytona/archil` and hands ownership to the `daytona` user so non-root processes can read and write through the mount.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
mount_path = "/home/daytona/archil"
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-archil",
env_vars={
"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"],
"ARCHIL_REGION": os.environ["ARCHIL_REGION"],
"ARCHIL_DISK": os.environ["ARCHIL_DISK"],
},
)
)
sandbox.process.exec(f"sudo mkdir -p {mount_path}")
sandbox.process.exec(
f"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
f"$ARCHIL_DISK {mount_path} --region $ARCHIL_REGION"
)
sandbox.process.exec(f"sudo chown daytona:daytona {mount_path}")
# Read and write through the mount as if it were a local directory
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const mountPath = '/home/daytona/archil'
const sandbox = await daytona.create({
snapshot: 'fuse-archil',
envVars: {
ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN!,
ARCHIL_REGION: process.env.ARCHIL_REGION!,
ARCHIL_DISK: process.env.ARCHIL_DISK!,
},
})
await sandbox.process.executeCommand(`sudo mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount ` +
`$ARCHIL_DISK ${mountPath} --region $ARCHIL_REGION`,
)
await sandbox.process.executeCommand(`sudo chown daytona:daytona ${mountPath}`)
// Read and write through the mount as if it were a local directory
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
mount_path = '/home/daytona/archil'
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-archil',
env_vars: {
'ARCHIL_MOUNT_TOKEN' => ENV.fetch('ARCHIL_MOUNT_TOKEN'),
'ARCHIL_REGION' => ENV.fetch('ARCHIL_REGION'),
'ARCHIL_DISK' => ENV.fetch('ARCHIL_DISK')
}
)
)
sandbox.process.exec(command: "sudo mkdir -p #{mount_path}")
sandbox.process.exec(
command: "sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount " \
"$ARCHIL_DISK #{mount_path} --region $ARCHIL_REGION"
)
sandbox.process.exec(command: "sudo chown daytona:daytona #{mount_path}")
# Read and write through the mount as if it were a local directory
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/archil"
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-archil",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"ARCHIL_MOUNT_TOKEN": os.Getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION": os.Getenv("ARCHIL_REGION"),
"ARCHIL_DISK": os.Getenv("ARCHIL_DISK"),
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "sudo mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "+
"$ARCHIL_DISK "+mountPath+" --region $ARCHIL_REGION"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo chown daytona:daytona "+mountPath); err != nil {
log.Fatal(err)
}
// Read and write through the mount as if it were a local directory
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String mountPath = "/home/daytona/archil";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-archil");
params.setEnvVars(Map.of(
"ARCHIL_MOUNT_TOKEN", System.getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION", System.getenv("ARCHIL_REGION"),
"ARCHIL_DISK", System.getenv("ARCHIL_DISK")
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand("sudo mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
+ "$ARCHIL_DISK " + mountPath + " --region $ARCHIL_REGION");
sandbox.getProcess().executeCommand("sudo chown daytona:daytona " + mountPath);
// Read and write through the mount as if it were a local directory
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
To let multiple sandboxes mount the **same** disk concurrently, add `--shared` to `archil mount`. In shared mode you check out paths with `archil checkout ` before writing, and check them back in with `archil checkin ` when done. See [Shared Disks ↗](https://docs.archil.com/concepts/shared-disks).
### Runtime install
Start from a default sandbox and install the `archil` CLI during startup before mounting the disk. This is useful when iterating quickly on mount behavior, with the tradeoff of slower cold starts for each sandbox.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
mount_path = "/home/daytona/archil"
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"],
"ARCHIL_REGION": os.environ["ARCHIL_REGION"],
"ARCHIL_DISK": os.environ["ARCHIL_DISK"],
},
)
)
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates"
)
sandbox.process.exec("curl -fsSL https://archil.com/install | sh")
sandbox.process.exec(f"sudo mkdir -p {mount_path}")
sandbox.process.exec(
f"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
f"$ARCHIL_DISK {mount_path} --region $ARCHIL_REGION"
)
sandbox.process.exec(f"sudo chown daytona:daytona {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const mountPath = '/home/daytona/archil'
const sandbox = await daytona.create({
envVars: {
ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN!,
ARCHIL_REGION: process.env.ARCHIL_REGION!,
ARCHIL_DISK: process.env.ARCHIL_DISK!,
},
})
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
)
await sandbox.process.executeCommand('curl -fsSL https://archil.com/install | sh')
await sandbox.process.executeCommand(`sudo mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount ` +
`$ARCHIL_DISK ${mountPath} --region $ARCHIL_REGION`,
)
await sandbox.process.executeCommand(`sudo chown daytona:daytona ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
mount_path = '/home/daytona/archil'
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'ARCHIL_MOUNT_TOKEN' => ENV.fetch('ARCHIL_MOUNT_TOKEN'),
'ARCHIL_REGION' => ENV.fetch('ARCHIL_REGION'),
'ARCHIL_DISK' => ENV.fetch('ARCHIL_DISK')
}
)
)
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates'
)
sandbox.process.exec(command: 'curl -fsSL https://archil.com/install | sh')
sandbox.process.exec(command: "sudo mkdir -p #{mount_path}")
sandbox.process.exec(
command: "sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount " \
"$ARCHIL_DISK #{mount_path} --region $ARCHIL_REGION"
)
sandbox.process.exec(command: "sudo chown daytona:daytona #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/archil"
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"ARCHIL_MOUNT_TOKEN": os.Getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION": os.Getenv("ARCHIL_REGION"),
"ARCHIL_DISK": os.Getenv("ARCHIL_DISK"),
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"curl -fsSL https://archil.com/install | sh"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "sudo mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "+
"$ARCHIL_DISK "+mountPath+" --region $ARCHIL_REGION"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo chown daytona:daytona "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxBaseParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String mountPath = "/home/daytona/archil";
CreateSandboxBaseParams params = new CreateSandboxBaseParams();
params.setEnvVars(Map.of(
"ARCHIL_MOUNT_TOKEN", System.getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION", System.getenv("ARCHIL_REGION"),
"ARCHIL_DISK", System.getenv("ARCHIL_DISK")
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates");
sandbox.getProcess().executeCommand(
"curl -fsSL https://archil.com/install | sh");
sandbox.getProcess().executeCommand("sudo mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
+ "$ARCHIL_DISK " + mountPath + " --region $ARCHIL_REGION");
sandbox.getProcess().executeCommand("sudo chown daytona:daytona " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount a MesaFS filesystem
[MesaFS ↗](https://mesa.dev) is an agent-native versioned filesystem from Mesa, purpose-built for the same workloads Daytona sandboxes run — parallel agent swarms, shared working memory, structured artifacts, and long-lived state across runs. With MesaFS, instead of mounting a cloud bucket, you mount a Mesa **repository**: a Git-compatible versioned working directory with sub-50ms reads/writes, instant fork/branch operations, and unlimited concurrent writers.
The Mesa setup follows the same pattern as the bucket providers but uses the Mesa CLI rather than a FUSE-specific tool: install the CLI in your sandbox, authenticate with your API key, and run `mesa mount --daemonize` to mount your repos at `/home/daytona/mesa/mnt//`.
**Credentials** — set `MESA_API_KEY` and `MESA_ORG` (your Mesa organization slug) in your local environment. The snippets below pass them into the sandbox via `envVars`, and the Mesa CLI reads them from there.
### Pre-built snapshot
Build a snapshot with the Mesa CLI preinstalled, then launch Mesa-enabled sandboxes from that snapshot. You still authenticate and mount at runtime, but installation is no longer part of each sandbox startup sequence.
#### Build a snapshot
Create a reusable snapshot that installs the Mesa CLI and enables the FUSE `user_allow_other` setting. Sandboxes launched from `fuse-mesa` can then authenticate and mount repos without repeating install work.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"curl -fsSL https://mesa.dev/install.sh | sh",
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-mesa", image=image),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'curl -fsSL https://mesa.dev/install.sh | sh',
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
await daytona.snapshot.create(
{ name: 'fuse-mesa', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'curl -fsSL https://mesa.dev/install.sh | sh',
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-mesa', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("curl -fsSL https://mesa.dev/install.sh | sh").
Run(`sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-mesa",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"curl -fsSL https://mesa.dev/install.sh | sh",
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
);
daytona.snapshot().create("fuse-mesa", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass `MESA_API_KEY` and your Mesa organization slug to the sandbox via `envVars`. Your code then writes a TOML config into the sandbox, authenticates the Mesa CLI, and mounts your repos at `/home/daytona/mesa/mnt//`.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
org = os.environ["MESA_ORG"]
repo = "my-workspace"
mount_path = f"/home/daytona/mesa/mnt/{org}/{repo}"
config_path = "/home/daytona/.config/mesa/config.toml"
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-mesa",
env_vars={
"MESA_API_KEY": os.environ["MESA_API_KEY"],
"MESA_ORG": org,
},
)
)
config = f'''mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.{org}]
'''
sandbox.process.exec(f"mkdir -p $(dirname {config_path})")
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec("mesa auth set-key --org $MESA_ORG $MESA_API_KEY")
sandbox.process.exec("mesa mount --daemonize")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const org = process.env.MESA_ORG!
const repo = 'my-workspace'
const mountPath = `/home/daytona/mesa/mnt/${org}/${repo}`
const configPath = '/home/daytona/.config/mesa/config.toml'
const sandbox = await daytona.create({
snapshot: 'fuse-mesa',
envVars: {
MESA_API_KEY: process.env.MESA_API_KEY!,
MESA_ORG: org,
},
})
const config = `mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.${org}]
`
await sandbox.process.executeCommand(`mkdir -p $(dirname ${configPath})`)
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand('mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
await sandbox.process.executeCommand('mesa mount --daemonize')
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
org = ENV.fetch('MESA_ORG')
repo = 'my-workspace'
mount_path = "/home/daytona/mesa/mnt/#{org}/#{repo}"
config_path = '/home/daytona/.config/mesa/config.toml'
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-mesa',
env_vars: {
'MESA_API_KEY' => ENV.fetch('MESA_API_KEY'),
'MESA_ORG' => org
}
)
)
config = <<~TOML
mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.#{org}]
TOML
sandbox.process.exec(command: "mkdir -p $(dirname #{config_path})")
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: 'mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
sandbox.process.exec(command: 'mesa mount --daemonize')
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
org := os.Getenv("MESA_ORG")
repo := "my-workspace"
mountPath := fmt.Sprintf("/home/daytona/mesa/mnt/%s/%s", org, repo)
configPath := "/home/daytona/.config/mesa/config.toml"
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-mesa",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"MESA_API_KEY": os.Getenv("MESA_API_KEY"),
"MESA_ORG": org,
},
},
})
if err != nil {
log.Fatal(err)
}
config := fmt.Sprintf(`mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.%s]
`, org)
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p $(dirname "+configPath+")"); err != nil {
log.Fatal(err)
}
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa auth set-key --org $MESA_ORG $MESA_API_KEY"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa mount --daemonize"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String org = System.getenv("MESA_ORG");
String repo = "my-workspace";
String mountPath = "/home/daytona/mesa/mnt/" + org + "/" + repo;
String configPath = "/home/daytona/.config/mesa/config.toml";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-mesa");
params.setEnvVars(Map.of(
"MESA_API_KEY", System.getenv("MESA_API_KEY"),
"MESA_ORG", org
));
Sandbox sandbox = daytona.create(params);
String config = "mount-point = \"/home/daytona/mesa/mnt\"\n\n"
+ "[secrets]\n"
+ "backend = \"plaintext-file\"\n\n"
+ "[organizations." + org + "]\n";
sandbox.getProcess().executeCommand("mkdir -p $(dirname " + configPath + ")");
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand(
"mesa auth set-key --org $MESA_ORG $MESA_API_KEY");
sandbox.getProcess().executeCommand("mesa mount --daemonize");
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install the Mesa CLI during startup before configuring auth and running `mesa mount --daemonize`. This is useful when iterating quickly on mount behavior, with the tradeoff of slower cold starts for each sandbox.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
org = os.environ["MESA_ORG"]
repo = "my-workspace"
mount_path = f"/home/daytona/mesa/mnt/{org}/{repo}"
config_path = "/home/daytona/.config/mesa/config.toml"
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"MESA_API_KEY": os.environ["MESA_API_KEY"],
"MESA_ORG": org,
},
)
)
sandbox.process.exec("curl -fsSL https://mesa.dev/install.sh | sh")
sandbox.process.exec(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
config = f'''mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.{org}]
'''
sandbox.process.exec(f"mkdir -p $(dirname {config_path})")
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec("mesa auth set-key --org $MESA_ORG $MESA_API_KEY")
sandbox.process.exec("mesa mount --daemonize")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const org = process.env.MESA_ORG!
const repo = 'my-workspace'
const mountPath = `/home/daytona/mesa/mnt/${org}/${repo}`
const configPath = '/home/daytona/.config/mesa/config.toml'
const sandbox = await daytona.create({
envVars: {
MESA_API_KEY: process.env.MESA_API_KEY!,
MESA_ORG: org,
},
})
await sandbox.process.executeCommand('curl -fsSL https://mesa.dev/install.sh | sh')
await sandbox.process.executeCommand(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
const config = `mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.${org}]
`
await sandbox.process.executeCommand(`mkdir -p $(dirname ${configPath})`)
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand('mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
await sandbox.process.executeCommand('mesa mount --daemonize')
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
org = ENV.fetch('MESA_ORG')
repo = 'my-workspace'
mount_path = "/home/daytona/mesa/mnt/#{org}/#{repo}"
config_path = '/home/daytona/.config/mesa/config.toml'
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'MESA_API_KEY' => ENV.fetch('MESA_API_KEY'),
'MESA_ORG' => org
}
)
)
sandbox.process.exec(command: 'curl -fsSL https://mesa.dev/install.sh | sh')
sandbox.process.exec(
command: "sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
config = <<~TOML
mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.#{org}]
TOML
sandbox.process.exec(command: "mkdir -p $(dirname #{config_path})")
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: 'mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
sandbox.process.exec(command: 'mesa mount --daemonize')
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
org := os.Getenv("MESA_ORG")
repo := "my-workspace"
mountPath := fmt.Sprintf("/home/daytona/mesa/mnt/%s/%s", org, repo)
configPath := "/home/daytona/.config/mesa/config.toml"
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"MESA_API_KEY": os.Getenv("MESA_API_KEY"),
"MESA_ORG": org,
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"curl -fsSL https://mesa.dev/install.sh | sh"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf`); err != nil {
log.Fatal(err)
}
config := fmt.Sprintf(`mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.%s]
`, org)
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p $(dirname "+configPath+")"); err != nil {
log.Fatal(err)
}
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa auth set-key --org $MESA_ORG $MESA_API_KEY"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa mount --daemonize"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String org = System.getenv("MESA_ORG");
String repo = "my-workspace";
String mountPath = "/home/daytona/mesa/mnt/" + org + "/" + repo;
String configPath = "/home/daytona/.config/mesa/config.toml";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"MESA_API_KEY", System.getenv("MESA_API_KEY"),
"MESA_ORG", org
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand(
"curl -fsSL https://mesa.dev/install.sh | sh");
sandbox.getProcess().executeCommand(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf");
String config = "mount-point = \"/home/daytona/mesa/mnt\"\n\n"
+ "[secrets]\n"
+ "backend = \"plaintext-file\"\n\n"
+ "[organizations." + org + "]\n";
sandbox.getProcess().executeCommand("mkdir -p $(dirname " + configPath + ")");
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand(
"mesa auth set-key --org $MESA_ORG $MESA_API_KEY");
sandbox.getProcess().executeCommand("mesa mount --daemonize");
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Production: scoped ephemeral keys
For non-test workloads, Mesa recommends minting a **short-lived, scoped API key per sandbox session** rather than passing your long-lived `MESA_API_KEY` into the sandbox. Use the [Mesa SDK ↗](https://docs.mesa.dev) on your trusted host to derive an ephemeral key from your long-lived one — the long-lived key never leaves your host process. Mesa SDKs are available for TypeScript, Python, and Rust; for other languages, use the [Mesa REST API ↗](https://docs.mesa.dev/content/api-reference/overview) directly.
```python
import asyncio
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
from mesa_sdk import Mesa
async def mint_ephemeral_key() -> str:
async with Mesa(api_key=os.environ["MESA_API_KEY"], org=os.environ["MESA_ORG"]) as mesa:
key = await mesa.api_keys.create(
name="sandbox-session",
scopes=["read", "write"],
expires_in_seconds=3600,
)
return key.key
ephemeral_key = asyncio.run(mint_ephemeral_key())
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-mesa",
env_vars={
"MESA_API_KEY": ephemeral_key,
"MESA_ORG": os.environ["MESA_ORG"],
},
)
)
```
```typescript
import { Daytona } from '@daytona/sdk'
import { Mesa } from '@mesadev/sdk'
const mesa = new Mesa({ apiKey: process.env.MESA_API_KEY!, org: process.env.MESA_ORG! })
const ephemeralKey = await mesa.apiKeys.create({
name: 'sandbox-session',
scopes: ['read', 'write'],
expires_in_seconds: 3600,
})
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-mesa',
envVars: {
MESA_API_KEY: ephemeralKey.key,
MESA_ORG: process.env.MESA_ORG!,
},
})
```
The rest of the launch flow (writing the TOML config, `mesa auth set-key`, `mesa mount --daemonize`) is unchanged — the sandbox doesn't know whether the key it received is long-lived or ephemeral.
For repo-scoped or path-scoped keys, see Mesa's [auth and permissions guide ↗](https://docs.mesa.dev/content/getting-started/auth-and-permissions). For the full integration recipe, see Mesa's [Daytona guide ↗](https://docs.mesa.dev/content/integration-guides/daytona).
## Unmount
When a sandbox is deleted via `daytona.delete(sandbox)`, the container teardown automatically removes any active FUSE mounts and shuts down their daemons. For normal cleanup, this is all you need — no manual unmount required.
To free a mount path **during** a sandbox's lifetime (for example, to remount with different credentials or before persisting a workspace archive), relocate the mount onto a throwaway path:
```bash
sudo mkdir -p /tmp/.fuse-defunct-$$
sudo mount --move /tmp/.fuse-defunct-$$
```
After this, your original mount path is free for remounting. The FUSE daemon stays alive serving the mount at the new path; both the relocated mount and the daemon are cleaned up automatically when the sandbox is deleted.
This works for any FUSE-based mount — verified against `mount-s3`, `gcsfuse`, and `blobfuse2`.
# Regions
Sandboxes are isolated runtime environments that run on [runners](https://www.daytona.io/docs/en/runners.md) — machines that form Daytona's compute plane.
Runners are organized into **regions**, which are geographic or logical groupings of compute infrastructure. When creating a sandbox, you can target a specific region, and Daytona will schedule your workload on an available runner within that region.
As a result, you're able to:
- Choose specific geographic locations for reduced latency
- Comply with data residency requirements
- Use your own runner machines for custom regions
- Scale compute resources independently within each custom region
Regions are geographic or logical groupings of runners that execute sandbox workloads. The sandbox region is specified by setting the `target` parameter on initialization:
```python
from daytona import Daytona, DaytonaConfig
# Configure Daytona to use the US region
config = DaytonaConfig(
target="us"
)
# Initialize the Daytona client with the specified configuration
daytona = Daytona(config)
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configure Daytona to use the EU region
const daytona: Daytona = new Daytona({
target: "eu"
});
```
```go
package main
import (
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
// Configure Daytona to use the US region
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us",
})
```
```ruby
require 'daytona'
# Configure Daytona to use the EU region
config = Daytona::Config.new(
target: "eu"
)
# Initialize the Daytona client with the specified configuration
daytona = Daytona::Daytona.new(config)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
// Configure Daytona to use the US region
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us")
.build();
Daytona daytona = new Daytona(config);
```
### Shared regions
Shared regions are managed by Daytona and available to all organizations. These regions provide immediate access to Daytona's infrastructure without any setup required.
Limits are applied to your organization's default region. For access to a different shared region, contact [sales@daytona.io](mailto:sales@daytona.io).
| Region | Target |
| ------------- | -------- |
| United States | **`us`** |
| Europe | **`eu`** |
### Dedicated regions
Dedicated regions are managed by Daytona and provisioned exclusively for individual organizations. These regions deliver dedicated infrastructure with the operational simplicity of a managed service.
:::note
Contact [sales@daytona.io](mailto:sales@daytona.io) to set up a dedicated region for your organization.
:::
### Custom regions
Custom regions are created and managed by your organization, allowing you to use your own runner machines and scale compute resources independently within each region. This provides maximum control over data locality, compliance, and infrastructure configuration.
Additionally, custom regions have no limits applied for concurrent resource usage, giving you full control over capacity and performance.
For more information, see the [runners](https://www.daytona.io/docs/en/runners.md) guide.
# File System Operations
Daytona provides comprehensive file system operations through the `fs` module in sandboxes.
## Basic operations
Daytona provides methods to interact with the file system in sandboxes. You can perform various operations like listing files, creating directories, reading and writing files, and more.
File operations assume you are operating in the sandbox user's home directory (e.g. `workspace` implies `/home/[username]/workspace`). Use a leading `/` when providing absolute paths.
### List files and directories
Daytona provides methods to list files and directories in a sandbox by providing the path to the directory. If the path is not provided, the method will list the files and directories in the sandbox working directory.
```python
# List files in a directory
files = sandbox.fs.list_files("workspace")
for file in files:
print(f"Name: {file.name}")
print(f"Is directory: {file.is_dir}")
print(f"Size: {file.size}")
print(f"Modified: {file.mod_time}")
```
```typescript
// List files in a directory
const files = await sandbox.fs.listFiles('workspace')
files.forEach(file => {
console.log(`Name: ${file.name}`)
console.log(`Is directory: ${file.isDir}`)
console.log(`Size: ${file.size}`)
console.log(`Modified: ${file.modTime}`)
})
```
```ruby
# List directory contents
files = sandbox.fs.list_files("workspace/data")
# Print files and their sizes
files.each do |file|
puts "#{file.name}: #{file.size} bytes" unless file.is_dir
end
# List only directories
dirs = files.select(&:is_dir)
puts "Subdirectories: #{dirs.map(&:name).join(', ')}"
```
```go
// List files in a directory
files, err := sandbox.FileSystem.ListFiles(ctx, "workspace")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
fmt.Printf("Name: %s\n", file.Name)
fmt.Printf("Is directory: %t\n", file.IsDirectory)
fmt.Printf("Size: %d\n", file.Size)
fmt.Printf("Modified: %s\n", file.ModifiedTime)
}
```
```java
import io.daytona.sdk.model.FileInfo;
import java.util.List;
List files = sandbox.fs.listFiles("workspace");
for (FileInfo file : files) {
System.out.println("Name: " + file.getName());
System.out.println("Is directory: " + file.getIsDir());
System.out.println("Size: " + file.getSize());
System.out.println("Modified: " + file.getModTime());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files'
```
### Get directory or file information
Daytona provides methods to get directory or file information such as group, directory, modified time, mode, name, owner, permissions, and size by providing the path to the directory or file.
```python
# Get file metadata
info = sandbox.fs.get_file_info("workspace/data/file.txt")
print(f"Size: {info.size} bytes")
print(f"Modified: {info.mod_time}")
print(f"Mode: {info.mode}")
# Check if path is a directory
info = sandbox.fs.get_file_info("workspace/data")
if info.is_dir:
print("Path is a directory")
```
```typescript
// Get file details
const info = await fs.getFileDetails('app/config.json')
console.log(`Size: ${info.size}, Modified: ${info.modTime}`)
```
```ruby
# Get file metadata
info = sandbox.fs.get_file_info("workspace/data/file.txt")
puts "Size: #{info.size} bytes"
puts "Modified: #{info.mod_time}"
puts "Mode: #{info.mode}"
# Check if path is a directory
info = sandbox.fs.get_file_info("workspace/data")
puts "Path is a directory" if info.is_dir
```
```go
// Get file metadata
info, err := sandbox.FileSystem.GetFileInfo(ctx, "workspace/data/file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Size: %d bytes\n", info.Size)
fmt.Printf("Modified: %s\n", info.ModifiedTime)
fmt.Printf("Mode: %s\n", info.Mode)
// Check if path is a directory
info, err = sandbox.FileSystem.GetFileInfo(ctx, "workspace/data")
if err != nil {
log.Fatal(err)
}
if info.IsDirectory {
fmt.Println("Path is a directory")
}
```
```java
import io.daytona.sdk.model.FileInfo;
FileInfo info = sandbox.fs.getFileDetails("workspace/data/file.txt");
System.out.println("Size: " + info.getSize() + " bytes");
System.out.println("Modified: " + info.getModTime());
System.out.println("Mode: " + info.getMode());
info = sandbox.fs.getFileDetails("workspace/data");
if (Boolean.TRUE.equals(info.getIsDir())) {
System.out.println("Path is a directory");
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/info?path='
```
### Create directories
Daytona provides methods to create directories by providing the path to the directory and the permissions to set on the directory.
```python
# Create with specific permissions
sandbox.fs.create_folder("workspace/new-dir", "755")
```
```typescript
// Create with specific permissions
await sandbox.fs.createFolder('workspace/new-dir', '755')
```
```ruby
# Create a directory with standard permissions
sandbox.fs.create_folder("workspace/data", "755")
# Create a private directory
sandbox.fs.create_folder("workspace/secrets", "700")
```
```go
// Create with specific permissions
err := sandbox.FileSystem.CreateFolder(ctx, "workspace/new-dir",
options.WithMode("755"),
)
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.createFolder("workspace/new-dir", "755");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/folder?path=&mode=' \
--request POST
```
### Upload files
Daytona provides methods to upload a single or multiple files in sandboxes.
#### Upload a single file
Daytona provides methods to upload a single file in sandboxes by providing the content to upload and the path to the file to upload it to.
```python
# Upload a single file
with open("local_file.txt", "rb") as f:
content = f.read()
sandbox.fs.upload_file(content, "remote_file.txt")
```
```typescript
// Upload a single file
const fileContent = Buffer.from('Hello, World!')
await sandbox.fs.uploadFile(fileContent, 'data.txt')
```
```ruby
# Upload a text file from string content
content = "Hello, World!"
sandbox.fs.upload_file(content, "tmp/hello.txt")
# Upload a local file
sandbox.fs.upload_file("local_file.txt", "tmp/file.txt")
# Upload binary data
data = { key: "value" }.to_json
sandbox.fs.upload_file(data, "tmp/config.json")
```
```go
// Upload from a local file path
err := sandbox.FileSystem.UploadFile(ctx, "local_file.txt", "remote_file.txt")
if err != nil {
log.Fatal(err)
}
// Or upload from byte content
content := []byte("Hello, World!")
err = sandbox.FileSystem.UploadFile(ctx, content, "hello.txt")
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
byte[] fileContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
sandbox.fs.uploadFile(fileContent, "data.txt");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/upload?path=' \
--request POST \
--header 'Content-Type: multipart/form-data' \
--form 'file='
```
#### Upload multiple files
Daytona provides methods to upload multiple files in sandboxes by providing the content to upload and their destination paths.
```python
# Upload multiple files at once
files_to_upload = []
with open("file1.txt", "rb") as f1:
files_to_upload.append(FileUpload(
source=f1.read(),
destination="data/file1.txt",
))
with open("file2.txt", "rb") as f2:
files_to_upload.append(FileUpload(
source=f2.read(),
destination="data/file2.txt",
))
with open("settings.json", "rb") as f3:
files_to_upload.append(FileUpload(
source=f3.read(),
destination="config/settings.json",
))
sandbox.fs.upload_files(files_to_upload)
```
```typescript
// Upload multiple files at once
const files = [
{
source: Buffer.from('Content of file 1'),
destination: 'data/file1.txt',
},
{
source: Buffer.from('Content of file 2'),
destination: 'data/file2.txt',
},
{
source: Buffer.from('{"key": "value"}'),
destination: 'config/settings.json',
},
]
await sandbox.fs.uploadFiles(files)
```
```ruby
# Upload multiple files
files = [
FileUpload.new("Content of file 1", "/tmp/file1.txt"),
FileUpload.new("workspace/data/file2.txt", "/tmp/file2.txt"),
FileUpload.new('{"key": "value"}', "/tmp/config.json")
]
sandbox.fs.upload_files(files)
```
```go
// Upload multiple files by calling UploadFile for each
filesToUpload := []struct {
source string
destination string
}{
{"file1.txt", "data/file1.txt"},
{"file2.txt", "data/file2.txt"},
{"settings.json", "config/settings.json"},
}
for _, f := range filesToUpload {
err := sandbox.FileSystem.UploadFile(ctx, f.source, f.destination)
if err != nil {
log.Fatal(err)
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/bulk-upload' \
--request POST
```
### Download files
Daytona provides methods to download files from sandboxes.
#### Download a single file
Daytona provides methods to download a single file from sandboxes by providing the path to the file to download.
```python
from daytona import DaytonaNotFoundError
try:
content = sandbox.fs.download_file("file1.txt")
except DaytonaNotFoundError as error:
print(f"Missing file: {error}")
else:
with open("local_file.txt", "wb") as f:
f.write(content)
print(content.decode("utf-8"))
```
```typescript
import { DaytonaNotFoundError } from '@daytona/sdk'
try {
const downloadedFile = await sandbox.fs.downloadFile('file1.txt')
console.log('File content:', downloadedFile.toString())
} catch (error) {
if (error instanceof DaytonaNotFoundError) {
console.error(`Missing file: ${error.message}`)
} else {
throw error
}
}
```
```ruby
# Download and get file content
content = sandbox.fs.download_file("workspace/data/file.txt")
puts content
# Download and save a file locally
sandbox.fs.download_file("workspace/data/file.txt", "local_copy.txt")
size_mb = File.size("local_copy.txt") / 1024.0 / 1024.0
puts "Size of the downloaded file: #{size_mb} MB"
```
```go
// Download and get contents in memory
content, err := sandbox.FileSystem.DownloadFile(ctx, "file1.txt", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
// Download and save to a local file
localPath := "local_file.txt"
content, err = sandbox.FileSystem.DownloadFile(ctx, "file1.txt", &localPath)
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
byte[] content = sandbox.fs.downloadFile("file1.txt");
System.out.println(new String(content, StandardCharsets.UTF_8));
Files.write(Path.of("local_file.txt"), content);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/download?path='
```
In the Python and TypeScript SDKs, `download_file` and `downloadFile` raise typed Daytona exceptions when the daemon returns structured per-file error metadata. Missing files map to not-found errors, invalid paths such as directories map to validation errors, and permission failures map to authorization errors.
#### Download multiple files
Daytona provides methods to download multiple files from sandboxes by providing the paths to the files to download.
```python
# Download multiple files at once
files_to_download = [
FileDownloadRequest(source="data/file1.txt"), # No destination - download to memory
FileDownloadRequest(source="data/file2.txt", destination="local_file2.txt"), # Download to local file
]
results = sandbox.fs.download_files(files_to_download)
for result in results:
if result.error:
print(f"Error downloading {result.source}: {result.error}")
if result.error_details:
print(
f" status={result.error_details.status_code} "
f"code={result.error_details.error_code}"
)
elif result.result:
print(f"Downloaded {result.source} to {result.result}")
```
```typescript
// Download multiple files at once
const files = [
{ source: 'data/file1.txt' }, // No destination - download to memory
{ source: 'data/file2.txt', destination: 'local_file2.txt' }, // Download to local file
]
const results = await sandbox.fs.downloadFiles(files)
results.forEach(result => {
if (result.error) {
console.error(`Error downloading ${result.source}: ${result.error}`)
if (result.errorDetails) {
console.error(
` status=${result.errorDetails.statusCode} code=${result.errorDetails.errorCode}`
)
}
} else if (result.result) {
console.log(`Downloaded ${result.source} to ${result.result}`)
}
})
```
```ruby
# Download multiple files by calling download_file for each
files_to_download = [
{ remote: "data/file1.txt", local: nil }, # Download to memory
{ remote: "data/file2.txt", local: "local_file2.txt" } # Download to local file
]
files_to_download.each do |f|
if f[:local]
sandbox.fs.download_file(f[:remote], f[:local])
puts "Downloaded #{f[:remote]} to #{f[:local]}"
else
content = sandbox.fs.download_file(f[:remote])
puts "Downloaded #{f[:remote]} to memory (#{content.size} bytes)"
end
end
```
```go
// Download multiple files by calling DownloadFile for each
filesToDownload := []struct {
remotePath string
localPath *string
}{
{"data/file1.txt", nil}, // Download to memory
{"data/file2.txt", ptrString("local_file2.txt")}, // Download to local file
}
for _, f := range filesToDownload {
content, err := sandbox.FileSystem.DownloadFile(ctx, f.remotePath, f.localPath)
if err != nil {
fmt.Printf("Error downloading %s: %v\n", f.remotePath, err)
continue
}
if f.localPath == nil {
fmt.Printf("Downloaded %s to memory (%d bytes)\n", f.remotePath, len(content))
} else {
fmt.Printf("Downloaded %s to %s\n", f.remotePath, *f.localPath)
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/bulk-download' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"paths": [
""
]
}'
```
Bulk downloads keep the existing `error` string for compatibility and now also include structured metadata on each failed item:
- Python: `result.error_details.message`, `result.error_details.status_code`, `result.error_details.error_code`
- TypeScript: `result.errorDetails.message`, `result.errorDetails.statusCode`, `result.errorDetails.errorCode`
The toolbox bulk-download API returns successful files as multipart `file` parts and per-file failures as multipart `error` parts with JSON payloads containing `message`, `statusCode`, and `code`.
### Delete files
Daytona provides methods to delete files or directories from sandboxes by providing the path to the file or directory to delete.
```python
sandbox.fs.delete_file("workspace/file.txt")
```
```typescript
await sandbox.fs.deleteFile('workspace/file.txt')
```
```ruby
# Delete a file
sandbox.fs.delete_file("workspace/data/old_file.txt")
# Delete a directory recursively
sandbox.fs.delete_file("workspace/old_dir", recursive: true)
```
```go
// Delete a file
err := sandbox.FileSystem.DeleteFile(ctx, "workspace/file.txt", false)
if err != nil {
log.Fatal(err)
}
// Delete a directory recursively
err = sandbox.FileSystem.DeleteFile(ctx, "workspace/old_dir", true)
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.deleteFile("workspace/file.txt");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files?path=' \
--request DELETE
```
## Advanced operations
Daytona provides advanced file system operations such as file permissions, search and replace, and move files.
### File permissions
Daytona provides methods to set file permissions, ownership, and group for a file or directory by providing the path to the file or directory and the permissions to set.
```python
# Set file permissions
sandbox.fs.set_file_permissions("workspace/file.txt", "644")
# Get file permissions
file_info = sandbox.fs.get_file_info("workspace/file.txt")
print(f"Permissions: {file_info.permissions}")
```
```typescript
// Set file permissions
await sandbox.fs.setFilePermissions('workspace/file.txt', { mode: '644' })
// Get file permissions
const fileInfo = await sandbox.fs.getFileDetails('workspace/file.txt')
console.log(`Permissions: ${fileInfo.permissions}`)
```
```ruby
# Make a file executable
sandbox.fs.set_file_permissions(
path: "workspace/scripts/run.sh",
mode: "755" # rwxr-xr-x
)
# Change file owner
sandbox.fs.set_file_permissions(
path: "workspace/data/file.txt",
owner: "daytona",
group: "daytona"
)
```
```go
// Set file permissions
err := sandbox.FileSystem.SetFilePermissions(ctx, "workspace/file.txt",
options.WithPermissionMode("644"),
)
if err != nil {
log.Fatal(err)
}
// Set owner and group
err = sandbox.FileSystem.SetFilePermissions(ctx, "workspace/file.txt",
options.WithOwner("daytona"),
options.WithGroup("daytona"),
)
if err != nil {
log.Fatal(err)
}
// Get file info to check permissions
fileInfo, err := sandbox.FileSystem.GetFileInfo(ctx, "workspace/file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mode: %s\n", fileInfo.Mode)
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/permissions?path=' \
--request POST
```
### Find and replace text in files
Daytona provides methods to find and replace text in files by providing the path to the directory to search in and the pattern to search for.
```python
# Search for text in files by providing the path to the directory to search in and the pattern to search for
results = sandbox.fs.find_files(
path="workspace/src",
pattern="text-of-interest"
)
for match in results:
print(f"Absolute file path: {match.file}")
print(f"Line number: {match.line}")
print(f"Line content: {match.content}")
print("\n")
# Replace text in files
sandbox.fs.replace_in_files(
files=["workspace/file1.txt", "workspace/file2.txt"],
pattern="old_text",
new_value="new_text"
)
```
```typescript
// Search for text in files; if a folder is specified, the search is recursive
const results = await sandbox.fs.findFiles({
path="workspace/src",
pattern: "text-of-interest"
})
results.forEach(match => {
console.log('Absolute file path:', match.file)
console.log('Line number:', match.line)
console.log('Line content:', match.content)
})
// Replace text in files
await sandbox.fs.replaceInFiles(
["workspace/file1.txt", "workspace/file2.txt"],
"old_text",
"new_text"
)
```
```ruby
# Search for TODOs in Ruby files
matches = sandbox.fs.find_files("workspace/src", "TODO:")
matches.each do |match|
puts "#{match.file}:#{match.line}: #{match.content.strip}"
end
# Replace in specific files
results = sandbox.fs.replace_in_files(
files: ["workspace/src/file1.rb", "workspace/src/file2.rb"],
pattern: "old_function",
new_value: "new_function"
)
# Print results
results.each do |result|
if result.success
puts "#{result.file}: #{result.success}"
else
puts "#{result.file}: #{result.error}"
end
end
```
```go
// Search for text in files
result, err := sandbox.FileSystem.FindFiles(ctx, "workspace/src", "text-of-interest")
if err != nil {
log.Fatal(err)
}
matches := result.([]map[string]any)
for _, match := range matches {
fmt.Printf("Absolute file path: %s\n", match["file"])
fmt.Printf("Line number: %v\n", match["line"])
fmt.Printf("Line content: %s\n\n", match["content"])
}
// Replace text in files
_, err = sandbox.FileSystem.ReplaceInFiles(ctx,
[]string{"workspace/file1.txt", "workspace/file2.txt"},
"old_text",
"new_text",
)
if err != nil {
log.Fatal(err)
}
```
```java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
List> results = sandbox.fs.findFiles("workspace/src", "text-of-interest");
for (Map match : results) {
System.out.println("Absolute file path: " + match.get("file"));
System.out.println("Line number: " + match.get("line"));
System.out.println("Line content: " + match.get("content"));
System.out.println();
}
sandbox.fs.replaceInFiles(
Arrays.asList("workspace/file1.txt", "workspace/file2.txt"),
"old_text",
"new_text"
);
```
Find text in files:
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/find?path=&pattern='
```
Replace text in files:
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/replace' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": [
""
],
"newValue": "",
"pattern": ""
}'
```
### Move or rename directory or file
Daytona provides methods to move or rename a directory or file in sandboxes by providing the path to the file or directory (source) and the new path to the file or directory (destination).
```python
# Rename a file
sandbox.fs.move_files(
"workspace/data/old_name.txt",
"workspace/data/new_name.txt"
)
# Move a file to a different directory
sandbox.fs.move_files(
"workspace/data/file.txt",
"workspace/archive/file.txt"
)
# Move a directory
sandbox.fs.move_files(
"workspace/old_dir",
"workspace/new_dir"
)
```
```typescript
// Move a file to a new location
await fs.moveFiles('app/temp/data.json', 'app/data/data.json')
```
```ruby
# Rename a file
sandbox.fs.move_files(
"workspace/data/old_name.txt",
"workspace/data/new_name.txt"
)
# Move a file to a different directory
sandbox.fs.move_files(
"workspace/data/file.txt",
"workspace/archive/file.txt"
)
# Move a directory
sandbox.fs.move_files(
"workspace/old_dir",
"workspace/new_dir"
)
```
```go
// Rename a file
err := sandbox.FileSystem.MoveFiles(ctx, "workspace/data/old_name.txt", "workspace/data/new_name.txt")
if err != nil {
log.Fatal(err)
}
// Move a file to a different directory
err = sandbox.FileSystem.MoveFiles(ctx, "workspace/data/file.txt", "workspace/archive/file.txt")
if err != nil {
log.Fatal(err)
}
// Move a directory
err = sandbox.FileSystem.MoveFiles(ctx, "workspace/old_dir", "workspace/new_dir")
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.moveFiles("workspace/data/old_name.txt", "workspace/data/new_name.txt");
sandbox.fs.moveFiles("workspace/data/file.txt", "workspace/archive/file.txt");
sandbox.fs.moveFiles("workspace/old_dir", "workspace/new_dir");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/move?source=&destination=' \
--request POST
```
# Git Operations
Daytona provides built-in Git support through the `git` module in sandboxes.
## Basic operations
Daytona provides methods to clone, check status, and manage Git repositories in sandboxes.
Similar to [file system operations](https://www.daytona.io/docs/en/file-system-operations.md), the starting cloning directory is the current sandbox working directory. It uses the WORKDIR specified in the Dockerfile if present, or falls back to the user's home directory if not - e.g. `workspace/repo` implies `/my-work-dir/workspace/repo`, but you are free to provide an absolute `workDir` path as well (by starting the path with `/`).
### Clone repositories
Daytona provides methods to clone Git repositories into sandboxes. You can clone public or private repositories, specific branches, and authenticate using personal access tokens.
Clones verify the remote's TLS certificate by default. For clones against internal Git servers that use self-signed or private-CA certificates, pass `insecure_skip_tls=true` (`insecureSkipTls: true` in TypeScript / Java). The bypass is per-request and disables TLS verification for that clone only; credentials, if supplied, are transmitted over an unverified TLS connection and are exposed to any MITM on the route. Prefer adding the server's CA to the sandbox base image's trust store when possible.
```python
# Basic clone
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo"
)
# Clone with authentication
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo",
username="git",
password="personal_access_token"
)
# Clone specific branch
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo",
branch="develop"
)
# Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
url="https://internal-git.example.com/org/repo.git",
path="workspace/repo",
insecure_skip_tls=True
)
```
```typescript
// Basic clone
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo"
);
// Clone with authentication
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
undefined,
undefined,
"git",
"personal_access_token"
);
// Clone specific branch
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
"develop"
);
// Clone from a self-signed internal Git server (insecure)
await sandbox.git.clone(
"https://internal-git.example.com/org/repo.git",
"workspace/repo",
undefined,
undefined,
undefined,
undefined,
true
);
```
```ruby
# Basic clone
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo'
)
# Clone with authentication
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo',
username: 'git',
password: 'personal_access_token'
)
# Clone specific branch
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo',
branch: 'develop'
)
# Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
url: 'https://internal-git.example.com/org/repo.git',
path: 'workspace/repo',
insecure_skip_tls: true
)
```
```go
// Basic clone
err := sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Clone with authentication
err = sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo",
options.WithUsername("git"),
options.WithPassword("personal_access_token"),
)
if err != nil {
log.Fatal(err)
}
// Clone specific branch
err = sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo",
options.WithBranch("develop"),
)
if err != nil {
log.Fatal(err)
}
// Clone from a self-signed internal Git server (insecure)
err = sandbox.Git.Clone(ctx, "https://internal-git.example.com/org/repo.git", "workspace/repo",
options.WithInsecureSkipTLS(true),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Basic clone
sandbox.git.clone("https://github.com/user/repo.git", "workspace/repo");
// Clone with authentication
sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
null,
null,
"git",
"personal_access_token"
);
// Clone specific branch
sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
"develop",
null,
null,
null
);
// Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
"https://internal-git.example.com/org/repo.git",
"workspace/repo",
null,
null,
null,
null,
true
);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/clone' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"branch": "",
"commit_id": "",
"insecure_skip_tls": false,
"password": "",
"path": "",
"url": "",
"username": ""
}'
```
### Get repository status
Daytona provides methods to check the status of Git repositories in sandboxes. You can get the current branch, modified files, number of commits ahead and behind main branch.
```python
# Get repository status
status = sandbox.git.status("workspace/repo")
print(f"Current branch: {status.current_branch}")
print(f"Commits ahead: {status.ahead}")
print(f"Commits behind: {status.behind}")
for file in status.file_status:
print(f"File: {file.name}")
# List branches
response = sandbox.git.branches("workspace/repo")
for branch in response.branches:
print(f"Branch: {branch}")
```
```typescript
// Get repository status
const status = await sandbox.git.status("workspace/repo");
console.log(`Current branch: ${status.currentBranch}`);
console.log(`Commits ahead: ${status.ahead}`);
console.log(`Commits behind: ${status.behind}`);
status.fileStatus.forEach(file => {
console.log(`File: ${file.name}`);
});
// List branches
const response = await sandbox.git.branches("workspace/repo");
response.branches.forEach(branch => {
console.log(`Branch: ${branch}`);
});
```
```ruby
# Get repository status
status = sandbox.git.status('workspace/repo')
puts "Current branch: #{status.current_branch}"
puts "Commits ahead: #{status.ahead}"
puts "Commits behind: #{status.behind}"
status.file_status.each do |file|
puts "File: #{file.name}"
end
# List branches
response = sandbox.git.branches('workspace/repo')
response.branches.each do |branch|
puts "Branch: #{branch}"
end
```
```go
// Get repository status
status, err := sandbox.Git.Status(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current branch: %s\n", status.CurrentBranch)
fmt.Printf("Commits ahead: %d\n", status.Ahead)
fmt.Printf("Commits behind: %d\n", status.Behind)
for _, file := range status.FileStatus {
fmt.Printf("File: %s\n", file.Path)
}
// List branches
branches, err := sandbox.Git.Branches(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
for _, branch := range branches {
fmt.Printf("Branch: %s\n", branch)
}
```
```java
import io.daytona.sdk.model.GitStatus;
import java.util.List;
// Get repository status
GitStatus status = sandbox.git.status("workspace/repo");
System.out.println("Current branch: " + status.getCurrentBranch());
System.out.println("Commits ahead: " + status.getAhead());
System.out.println("Commits behind: " + status.getBehind());
for (GitStatus.FileStatus file : status.getFileStatus()) {
System.out.println("File: " + file.getPath());
}
// List branches
Object rawBranches = sandbox.git.branches("workspace/repo").get("branches");
if (rawBranches instanceof List> branchList) {
for (Object branch : branchList) {
System.out.println("Branch: " + branch);
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/status?path='
```
## Branch operations
Daytona provides methods to manage branches in Git repositories. You can create, switch, and delete branches.
### Create branches
Daytona provides methods to create branches in Git repositories. The following snippet creates a new branch called `new-feature`.
```python
# Create a new branch
sandbox.git.create_branch("workspace/repo", "new-feature")
```
```typescript
// Create new branch
await git.createBranch('workspace/repo', 'new-feature');
```
```ruby
# Create a new branch
sandbox.git.create_branch('workspace/repo', 'new-feature')
```
```go
// Create a new branch
err := sandbox.Git.CreateBranch(ctx, "workspace/repo", "new-feature")
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/branches' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"name": "",
"path": ""
}'
```
### Checkout branches
Daytona provides methods to checkout branches in Git repositories. The following snippet checks out the branch called `feature-branch`.
```python
# Checkout a branch
sandbox.git.checkout_branch("workspace/repo", "feature-branch")
```
```typescript
// Checkout a branch
await git.checkoutBranch('workspace/repo', 'feature-branch');
```
```ruby
# Checkout a branch
sandbox.git.checkout_branch('workspace/repo', 'feature-branch')
```
```go
// Checkout a branch
err := sandbox.Git.Checkout(ctx, "workspace/repo", "feature-branch")
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/checkout' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"branch": "",
"path": ""
}'
```
### Delete branches
Daytona provides methods to delete branches in Git repositories. The following snippet deletes the branch called `old-feature`.
```python
# Delete a branch
sandbox.git.delete_branch("workspace/repo", "old-feature")
```
```typescript
// Delete a branch
await git.deleteBranch('workspace/repo', 'old-feature');
```
```ruby
# Delete a branch
sandbox.git.delete_branch('workspace/repo', 'old-feature')
```
```go
// Delete a branch
err := sandbox.Git.DeleteBranch(ctx, "workspace/repo", "old-feature")
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/branches' \
--request DELETE \
--header 'Content-Type: application/json' \
--data '{
"name": "",
"path": ""
}'
```
## Stage changes
Daytona provides methods to stage changes in Git repositories. You can stage specific files, all changes, and commit with a message. The following snippet stages the file `file.txt` and the `src` directory.
```python
# Stage a single file
sandbox.git.add("workspace/repo", ["file.txt"])
# Stage multiple files
sandbox.git.add("workspace/repo", [
"src/main.py",
"tests/test_main.py",
"README.md"
])
```
```typescript
// Stage a single file
await git.add('workspace/repo', ['file.txt']);
// Stage whole repository
await git.add('workspace/repo', ['.']);
```
```ruby
# Stage a single file
sandbox.git.add('workspace/repo', ['file.txt'])
```
```go
// Stage a single file
err := sandbox.Git.Add(ctx, "workspace/repo", []string{"file.txt"})
if err != nil {
log.Fatal(err)
}
// Stage multiple files
err = sandbox.Git.Add(ctx, "workspace/repo", []string{
"src/main.py",
"tests/test_main.py",
"README.md",
})
if err != nil {
log.Fatal(err)
}
// Stage whole repository
err = sandbox.Git.Add(ctx, "workspace/repo", []string{"."})
if err != nil {
log.Fatal(err)
}
```
```java
import java.util.List;
// Stage a single file
sandbox.git.add("workspace/repo", List.of("file.txt"));
// Stage multiple files
sandbox.git.add(
"workspace/repo",
List.of("src/main.py", "tests/test_main.py", "README.md")
);
// Stage whole repository
sandbox.git.add("workspace/repo", List.of("."));
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/add' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": [
""
],
"path": ""
}'
```
## Commit changes
Daytona provides methods to commit changes in Git repositories. You can commit with a message, author, and email. The following snippet commits the changes with the message `Update documentation` and the author `John Doe` and email `john@example.com`.
```python
# Stage and commit changes
sandbox.git.add("workspace/repo", ["README.md"])
sandbox.git.commit(
path="workspace/repo",
message="Update documentation",
author="John Doe",
email="john@example.com",
allow_empty=True
)
```
```typescript
// Stage and commit changes
await git.add('workspace/repo', ['README.md']);
await git.commit(
'workspace/repo',
'Update documentation',
'John Doe',
'john@example.com',
true
);
```
```ruby
# Stage and commit changes
sandbox.git.add('workspace/repo', ['README.md'])
sandbox.git.commit('workspace/repo', 'Update documentation', 'John Doe', 'john@example.com', true)
```
```go
// Stage and commit changes
err := sandbox.Git.Add(ctx, "workspace/repo", []string{"README.md"})
if err != nil {
log.Fatal(err)
}
response, err := sandbox.Git.Commit(ctx, "workspace/repo",
"Update documentation",
"John Doe",
"john@example.com",
options.WithAllowEmpty(true),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Commit SHA: %s\n", response.SHA)
```
```java
import io.daytona.sdk.model.GitCommitResponse;
import java.util.List;
// Stage and commit changes
sandbox.git.add("workspace/repo", List.of("README.md"));
GitCommitResponse response = sandbox.git.commit(
"workspace/repo",
"Update documentation",
"John Doe",
"john@example.com"
);
System.out.println("Commit hash: " + response.getHash());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/commit' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"allow_empty": true,
"author": "",
"email": "",
"message": "",
"path": ""
}'
```
## Remote operations
Daytona provides methods to work with remote repositories in Git. You can push and pull changes from remote repositories.
### Push changes
Daytona provides methods to push changes to remote repositories. The following snippet pushes the changes to a public repository.
```python
# Push without authentication (for public repos or SSH)
sandbox.git.push("workspace/repo")
# Push with authentication
sandbox.git.push(
path="workspace/repo",
username="user",
password="github_token"
)
```
```typescript
// Push to a public repository
await git.push('workspace/repo');
// Push to a private repository
await git.push(
'workspace/repo',
'user',
'token'
);
```
```ruby
# Push changes
sandbox.git.push('workspace/repo')
```
```go
// Push without authentication (for public repos or SSH)
err := sandbox.Git.Push(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Push with authentication
err = sandbox.Git.Push(ctx, "workspace/repo",
options.WithPushUsername("user"),
options.WithPushPassword("github_token"),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Push without authentication (for public repos or SSH)
sandbox.git.push("workspace/repo");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/push' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"password": "",
"path": "",
"username": ""
}'
```
### Pull changes
Daytona provides methods to pull changes from remote repositories. The following snippet pulls the changes from a public repository.
```python
# Pull without authentication
sandbox.git.pull("workspace/repo")
# Pull with authentication
sandbox.git.pull(
path="workspace/repo",
username="user",
password="github_token"
)
```
```typescript
// Pull from a public repository
await git.pull('workspace/repo');
// Pull from a private repository
await git.pull(
'workspace/repo',
'user',
'token'
);
```
```ruby
# Pull changes
sandbox.git.pull('workspace/repo')
```
```go
// Pull without authentication
err := sandbox.Git.Pull(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Pull with authentication
err = sandbox.Git.Pull(ctx, "workspace/repo",
options.WithPullUsername("user"),
options.WithPullPassword("github_token"),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Pull without authentication
sandbox.git.pull("workspace/repo");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/pull' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"password": "",
"path": "",
"username": ""
}'
```
# Language Server Protocol
Daytona provides Language Server Protocol (LSP) support through sandbox instances. This enables advanced language features like code completion, diagnostics, and more.
## Create LSP servers
Daytona provides methods to create LSP servers. The `path_to_project` argument is relative to the current sandbox working directory when no leading `/` is used. The working directory is specified by WORKDIR when it is present in the Dockerfile, and otherwise falls back to the user's home directory.
```python
from daytona import Daytona, LspLanguageId
# Create Sandbox
daytona = Daytona()
sandbox = daytona.create()
# Create LSP server for Python
lsp_server = sandbox.create_lsp_server(
language_id=LspLanguageId.PYTHON,
path_to_project="workspace/project"
)
```
```typescript
import { Daytona, LspLanguageId } from '@daytona/sdk'
// Create sandbox
const daytona = new Daytona()
const sandbox = await daytona.create({
language: 'typescript',
})
// Create LSP server for TypeScript
const lspServer = await sandbox.createLspServer(
LspLanguageId.TYPESCRIPT,
'workspace/project'
)
```
```ruby
require 'daytona'
# Create Sandbox
daytona = Daytona::Daytona.new
sandbox = daytona.create
# Create LSP server for Python
lsp_server = sandbox.create_lsp_server(
language_id: Daytona::LspServer::Language::PYTHON,
path_to_project: 'workspace/project'
)
```
```go
// Create sandbox
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
sandbox, err := client.Create(ctx, nil)
if err != nil {
log.Fatal(err)
}
// Create LSP server for Python
lsp := sandbox.CreateLspServer(types.LspLanguagePython, "workspace/project")
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.LspServer;
import io.daytona.sdk.Sandbox;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
LspServer lspServer = sandbox.createLspServer(
LspServer.LspLanguageId.PYTHON.getValue(),
"workspace/project");
}
}
}
```
### Supported languages
The supported languages for creating LSP servers with Daytona are defined by the `LspLanguageId` enum:
| Enum Value | Description |
| ------------------------------ | -------------------------------------- |
| **`LspLanguageId.PYTHON`** | Python language server |
| **`LspLanguageId.TYPESCRIPT`** | TypeScript/JavaScript language server |
## Start LSP servers
Daytona provides methods to start LSP servers.
```python
lsp = sandbox.create_lsp_server("typescript", "workspace/project")
lsp.start() # Initialize the server
# Now ready for LSP operations
```
```typescript
const lsp = await sandbox.createLspServer('typescript', 'workspace/project')
await lsp.start() // Initialize the server
// Now ready for LSP operations
```
```ruby
lsp = sandbox.create_lsp_server(
language_id: Daytona::LspServer::Language::PYTHON,
path_to_project: 'workspace/project'
)
lsp.start # Initialize the server
# Now ready for LSP operations
```
```go
lsp := sandbox.CreateLspServer(types.LspLanguagePython, "workspace/project")
err := lsp.Start(ctx) // Initialize the server
if err != nil {
log.Fatal(err)
}
// Now ready for LSP operations
```
```java
LspServer lsp = sandbox.createLspServer("typescript", "workspace/project");
lsp.start("typescript", "workspace/project");
// Now ready for LSP operations
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/start' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "",
"pathToProject": ""
}'
```
## Stop LSP servers
Daytona provides methods to stop LSP servers.
```python
# When done with LSP features
lsp.stop() # Clean up resources
```
```typescript
// When done with LSP features
await lsp.stop() // Clean up resources
```
```ruby
# When done with LSP features
lsp.stop # Clean up resources
```
```go
// When done with LSP features
err := lsp.Stop(ctx) // Clean up resources
if err != nil {
log.Fatal(err)
}
```
```java
// When done with LSP features
lsp.stop("typescript", "workspace/project"); // Clean up resources
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/stop' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "",
"pathToProject": ""
}'
```
## Code completions
Daytona provides methods to get code completions for a specific position in a file.
```python
completions = lsp_server.completions(
path="workspace/project/main.py",
position={"line": 10, "character": 15}
)
print(f"Completions: {completions}")
```
```typescript
const completions = await lspServer.completions('workspace/project/main.ts', {
line: 10,
character: 15,
})
console.log('Completions:', completions)
```
```ruby
completions = lsp_server.completions(
path: 'workspace/project/main.py',
position: { line: 10, character: 15 }
)
puts "Completions: #{completions}"
```
```go
completions, err := lsp.Completions(ctx, "workspace/project/main.py",
types.Position{Line: 10, Character: 15},
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Completions: %v\n", completions)
```
```java
var completions = lsp.completions(
"typescript",
"workspace/project",
"workspace/project/Main.java",
10,
15);
System.out.println("Completions: " + completions);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/completions' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"context": {
"triggerCharacter": "",
"triggerKind": 1
},
"languageId": "",
"pathToProject": "",
"position": {
"character": 1,
"line": 1
},
"uri": ""
}'
```
## File notifications
Daytona provides methods to notify the LSP server when files are opened or closed. This enables features like diagnostics and completion tracking for the specified files.
### Open file
Notifies the language server that a file has been opened for editing.
```python
# Notify server that a file is open
lsp_server.did_open("workspace/project/main.py")
```
```typescript
// Notify server that a file is open
await lspServer.didOpen('workspace/project/main.ts')
```
```ruby
# Notify server that a file is open
lsp_server.did_open('workspace/project/main.py')
```
```go
// Notify server that a file is open
err := lsp.DidOpen(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
```
```java
// Notify server that a file is open
lsp.didOpen("typescript", "workspace/project", "workspace/project/Main.java");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/did-open' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "",
"pathToProject": "",
"uri": ""
}'
```
### Close file
Notifies the language server that a file has been closed. This allows the server to clean up resources associated with that file.
```python
# Notify server that a file is closed
lsp_server.did_close("workspace/project/main.py")
```
```typescript
// Notify server that a file is closed
await lspServer.didClose('workspace/project/main.ts')
```
```ruby
# Notify server that a file is closed
lsp_server.did_close('workspace/project/main.py')
```
```go
// Notify server that a file is closed
err := lsp.DidClose(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
```
```java
// Notify server that a file is closed
lsp.didClose("typescript", "workspace/project", "workspace/project/Main.java");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/did-close' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "",
"pathToProject": "",
"uri": ""
}'
```
## Document symbols
Daytona provides methods to retrieve symbols (functions, classes, variables, etc.) from a document.
```python
symbols = lsp_server.document_symbols("workspace/project/main.py")
for symbol in symbols:
print(f"Symbol: {symbol.name}, Kind: {symbol.kind}")
```
```typescript
const symbols = await lspServer.documentSymbols('workspace/project/main.ts')
symbols.forEach((symbol) => {
console.log(`Symbol: ${symbol.name}, Kind: ${symbol.kind}`)
})
```
```ruby
symbols = lsp_server.document_symbols('workspace/project/main.py')
symbols.each do |symbol|
puts "Symbol: #{symbol.name}, Kind: #{symbol.kind}"
end
```
```go
symbols, err := lsp.DocumentSymbols(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
for _, symbol := range symbols {
fmt.Printf("Symbol: %v\n", symbol)
}
```
```java
var symbols = lsp.documentSymbols(
"typescript",
"workspace/project",
"workspace/project/Main.java");
for (var symbol : symbols) {
System.out.println("Symbol: " + symbol.getName() + ", Kind: " + symbol.getKind());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/document-symbols?languageId=&pathToProject=&uri='
```
## Sandbox symbols
Daytona provides methods to search for symbols across all files in the sandbox.
```python
symbols = lsp_server.sandbox_symbols("MyClass")
for symbol in symbols:
print(f"Found: {symbol.name} at {symbol.location}")
```
```typescript
const symbols = await lspServer.sandboxSymbols('MyClass')
symbols.forEach((symbol) => {
console.log(`Found: ${symbol.name} at ${symbol.location}`)
})
```
```ruby
symbols = lsp_server.sandbox_symbols('MyClass')
symbols.each do |symbol|
puts "Found: #{symbol.name} at #{symbol.location}"
end
```
```go
symbols, err := lsp.SandboxSymbols(ctx, "MyClass")
if err != nil {
log.Fatal(err)
}
for _, symbol := range symbols {
fmt.Printf("Found: %v\n", symbol)
}
```
```java
var symbols = lsp.workspaceSymbols("MyClass", "typescript", "workspace/project");
for (var symbol : symbols) {
System.out.println("Found: " + symbol.getName() + " at " + symbol.getLocation());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/workspacesymbols?query=&languageId=&pathToProject='
```
# Process and Code Execution
Daytona provides process and code execution capabilities through the `process` module in sandboxes.
## Code execution
Daytona provides methods to execute code in sandboxes. You can run code snippets in multiple languages with support for both stateless execution and stateful interpretation with persistent contexts.
- [Run code (stateless)](#run-code-stateless): run independent code snippets where each execution starts from a clean interpreter state; inherits the sandbox language that you choose at [sandbox creation](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes).
- [Run code (stateful)](#run-code-stateful): run code in a persistent interpreter context with variables, imports, and state to carry across executions; executes Python code and is available for every SDK.
### Run code (stateless)
Daytona provides methods to run code snippets in sandboxes using stateless execution. Each invocation starts from a clean interpreter, making it ideal for independent code snippets.
```python
# Run Python code
response = sandbox.process.code_run('''
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
''')
print(response.result)
```
```typescript
// Run TypeScript code
let response = await sandbox.process.codeRun(`
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
console.log(greet("Daytona"));
`);
console.log(response.result);
// Run code with argv and environment variables
response = await sandbox.process.codeRun(
`
console.log(\`Hello, \${process.argv[2]}!\`);
console.log(\`FOO: \${process.env.FOO}\`);
`,
{
argv: ["Daytona"],
env: { FOO: "BAR" }
}
);
console.log(response.result);
// Run code with timeout (5 seconds)
response = await sandbox.process.codeRun(
'setTimeout(() => console.log("Done"), 2000);',
undefined,
5
);
console.log(response.result);
```
```ruby
# Run Python code
response = sandbox.process.code_run(code: <<~PYTHON)
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
PYTHON
puts response.result
```
```go
// Run code using shell command execution
// Note: For stateless code execution in Go, use ExecuteCommand with the appropriate interpreter
result, err := sandbox.Process.ExecuteCommand(ctx, `python3 -c '
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
'`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// Run code with environment variables
result, err = sandbox.Process.ExecuteCommand(ctx, `python3 -c 'import os; print(f"FOO: {os.environ.get(\"FOO\")}")'`,
options.WithCommandEnv(map[string]string{"FOO": "BAR"}),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// Run code with timeout
result, err = sandbox.Process.ExecuteCommand(ctx, `python3 -c 'import time; time.sleep(2); print("Done")'`,
options.WithExecuteTimeout(5*time.Second),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
```
```java
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
// Run code (stateless; language matches the sandbox image)
ExecuteResponse response = sandbox.process.codeRun(
"""
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
"""
);
System.out.println(response.getResult());
// Run code with environment variables and timeout (seconds)
response = sandbox.process.codeRun(
"import os; print('FOO:', os.environ.get('FOO'))",
Map.of("FOO", "BAR"),
null
);
System.out.println(response.getResult());
response = sandbox.process.codeRun(
"import time; time.sleep(2); print(\"Done\")",
null,
5
);
System.out.println(response.getResult());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/code-run' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"code": "def greet(name):\n return f\"Hello, {name}!\"\n\nprint(greet(\"Daytona\"))",
"env": {
"FOO": "BAR"
},
"timeout": 5000
}'
```
### Run code (stateful)
Daytona provides methods to run code with persistent state using the code interpreter. You can maintain variables and imports between calls, create isolated contexts, and control environment variables.
```python
from daytona import Daytona, OutputMessage
def handle_stdout(message: OutputMessage):
print(f"[STDOUT] {message.output}")
daytona = Daytona()
sandbox = daytona.create()
# Shared default context
result = sandbox.code_interpreter.run_code(
"counter = 1\nprint(f'Counter initialized at {counter}')",
on_stdout=handle_stdout,
)
# Isolated context
ctx = sandbox.code_interpreter.create_context()
try:
sandbox.code_interpreter.run_code(
"value = 'stored in ctx'",
context=ctx,
)
sandbox.code_interpreter.run_code(
"print(value)",
context=ctx,
on_stdout=handle_stdout,
)
finally:
sandbox.code_interpreter.delete_context(ctx)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
async function main() {
const sandbox = await daytona.create()
// Shared default context
await sandbox.codeInterpreter.runCode(
`
counter = 1
print(f'Counter initialized at {counter}')
`,
{ onStdout: (msg) => process.stdout.write(`[STDOUT] ${msg.output}`)},
)
// Isolated context
const ctx = await sandbox.codeInterpreter.createContext()
try {
await sandbox.codeInterpreter.runCode(
`value = 'stored in ctx'`,
{ context: ctx },
)
await sandbox.codeInterpreter.runCode(
`print(value)`,
{ context: ctx, onStdout: (msg) => process.stdout.write(`[STDOUT] ${msg.output}`) },
)
} finally {
await sandbox.codeInterpreter.deleteContext(ctx)
}
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
# Shared default context
sandbox.code_interpreter.run_code(
<<~PYTHON,
counter = 1
print(f'Counter initialized at {counter}')
PYTHON
on_stdout: ->(msg) { print "[STDOUT] #{msg.output}" }
)
# Isolated context
ctx = sandbox.code_interpreter.create_context
begin
sandbox.code_interpreter.run_code("value = 'stored in ctx'", context: ctx)
sandbox.code_interpreter.run_code(
"print(value)",
context: ctx,
on_stdout: ->(msg) { print "[STDOUT] #{msg.output}" }
)
ensure
sandbox.code_interpreter.delete_context(ctx)
end
```
Use `sandbox.process.exec` for one-shot shell commands. Use `sandbox.process.create_session` with `sandbox.process.execute_session_command` for persistent shell state, and stream output with `sandbox.process.get_session_command_logs_async`.
```go
// Create a code interpreter context
ctxInfo, err := sandbox.CodeInterpreter.CreateContext(ctx, nil)
if err != nil {
log.Fatal(err)
}
contextID := ctxInfo["id"].(string)
// Run code in the context
channels, err := sandbox.CodeInterpreter.RunCode(ctx,
"counter = 1\nprint(f'Counter initialized at {counter}')",
options.WithCustomContext(contextID),
)
if err != nil {
log.Fatal(err)
}
// Read output
for msg := range channels.Stdout {
fmt.Printf("[STDOUT] %s\n", msg.Text)
}
// Clean up context
err = sandbox.CodeInterpreter.DeleteContext(ctx, contextID)
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.RunCodeOptions;
// Default interpreter context (Python)
sandbox.codeInterpreter.runCode(
"""
counter = 1
print(f'Counter initialized at {counter}')
""",
new RunCodeOptions().setOnStdout(chunk -> System.out.print("[STDOUT] " + chunk))
);
```
```bash
# Create context
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/context' \
--request POST \
--header 'Content-Type: application/json' \
--data '{}'
# Run code in context (WebSocket endpoint)
# Connect via WebSocket to:
# wss://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/execute
# Send JSON message:
# {
# "code": "counter = 1\nprint(f\"Counter initialized at {counter}\")",
# "contextId": "your-context-id"
# }
# Delete context
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/context/{contextId}' \
--request DELETE
```
## Command execution
Daytona provides methods to execute shell commands in sandboxes. You can run commands with working directory, timeout, and environment variable options.
The working directory defaults to the sandbox working directory. It uses the WORKDIR specified in the Dockerfile if present, or falls back to the user's home directory if not (e.g., `workspace/repo` implies `/home/daytona/workspace/repo`). You can override it with an absolute path by starting the path with `/`.
### Execute commands
Daytona provides methods to execute shell commands in sandboxes by providing the command string and optional parameters for working directory, timeout, and environment variables. You can also use the `daytona exec` CLI command for quick command execution.
```python
# Execute any shell command
response = sandbox.process.exec("ls -la")
print(response.result)
# Setting a working directory and a timeout
response = sandbox.process.exec("sleep 3", cwd="workspace/src", timeout=5)
print(response.result)
# Passing environment variables
response = sandbox.process.exec("echo $CUSTOM_SECRET", env={
"CUSTOM_SECRET": "DAYTONA"
}
)
print(response.result)
```
```typescript
// Execute any shell command
const response = await sandbox.process.executeCommand("ls -la");
console.log(response.result);
// Setting a working directory and a timeout
const response2 = await sandbox.process.executeCommand("sleep 3", "workspace/src", undefined, 5);
console.log(response2.result);
// Passing environment variables
const response3 = await sandbox.process.executeCommand("echo $CUSTOM_SECRET", ".", {
"CUSTOM_SECRET": "DAYTONA"
}
);
console.log(response3.result);
```
```ruby
# Execute any shell command
response = sandbox.process.exec(command: 'ls -la')
puts response.result
# Setting a working directory and a timeout
response = sandbox.process.exec(command: 'sleep 3', cwd: 'workspace/src', timeout: 5)
puts response.result
# Passing environment variables
response = sandbox.process.exec(
command: 'echo $CUSTOM_SECRET',
env: { 'CUSTOM_SECRET' => 'DAYTONA' }
)
puts response.result
```
```go
// Execute any shell command
response, err := sandbox.Process.ExecuteCommand(ctx, "ls -la")
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
// Setting a working directory and a timeout
response, err = sandbox.Process.ExecuteCommand(ctx, "sleep 3",
options.WithCwd("workspace/src"),
options.WithExecuteTimeout(5*time.Second),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
// Passing environment variables
response, err = sandbox.Process.ExecuteCommand(ctx, "echo $CUSTOM_SECRET",
options.WithCommandEnv(map[string]string{"CUSTOM_SECRET": "DAYTONA"}),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
// Execute any shell command
ExecuteResponse response = sandbox.process.executeCommand("ls -la");
System.out.println(response.getResult());
// Working directory and timeout (seconds)
response = sandbox.process.executeCommand("sleep 3", "workspace/src", null, 5);
System.out.println(response.getResult());
// Environment variables
response = sandbox.process.executeCommand(
"echo $CUSTOM_SECRET",
".",
Map.of("CUSTOM_SECRET", "DAYTONA"),
null
);
System.out.println(response.getResult());
```
```bash
# Execute any shell command
daytona exec my-sandbox -- ls -la
# Setting a working directory and a timeout
daytona exec my-sandbox --cwd workspace/src --timeout 5 -- sleep 3
# Passing environment variables (use shell syntax)
daytona exec my-sandbox -- sh -c 'CUSTOM_SECRET=DAYTONA echo $CUSTOM_SECRET'
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "ls -la",
"cwd": "workspace",
"timeout": 5
}'
```
## Session operations
Daytona provides methods to manage background process sessions in sandboxes. You can create sessions, execute commands, monitor status, and manage long-running processes.
### Get session status
Daytona provides methods to get session status and list all sessions in a sandbox by providing the session ID.
```python
# Check session's executed commands
session = sandbox.process.get_session(session_id)
print(f"Session {session_id}:")
for command in session.commands:
print(f"Command: {command.command}, Exit Code: {command.exit_code}")
# List all running sessions
sessions = sandbox.process.list_sessions()
for session in sessions:
print(f"Session: {session.session_id}, Commands: {session.commands}")
```
```typescript
// Check session's executed commands
const session = await sandbox.process.getSession(sessionId);
console.log(`Session ${sessionId}:`);
for (const command of session.commands) {
console.log(`Command: ${command.command}, Exit Code: ${command.exitCode}`);
}
// List all running sessions
const sessions = await sandbox.process.listSessions();
for (const session of sessions) {
console.log(`Session: ${session.sessionId}, Commands: ${session.commands}`);
}
```
```ruby
# Check session's executed commands
session = sandbox.process.get_session(session_id)
puts "Session #{session_id}:"
session.commands.each do |command|
puts "Command: #{command.command}, Exit Code: #{command.exit_code}"
end
# List all running sessions
sessions = sandbox.process.list_sessions
sessions.each do |session|
puts "Session: #{session.session_id}, Commands: #{session.commands}"
end
```
```go
// Check session's executed commands
session, err := sandbox.Process.GetSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session %s:\n", sessionID)
commands := session["commands"].([]any)
for _, cmd := range commands {
cmdMap := cmd.(map[string]any)
fmt.Printf("Command: %s, Exit Code: %v\n", cmdMap["command"], cmdMap["exitCode"])
}
// List all running sessions
sessions, err := sandbox.Process.ListSessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, sess := range sessions {
fmt.Printf("Session: %s, Commands: %v\n", sess["sessionId"], sess["commands"])
}
```
```java
import io.daytona.sdk.model.Command;
import io.daytona.sdk.model.Session;
// Check session's executed commands
Session session = sandbox.process.getSession(sessionId);
System.out.println("Session " + sessionId + ":");
for (Command command : session.getCommands()) {
System.out.println("Command: " + command.getCommand() + ", Exit Code: " + command.getExitCode());
}
// List all running sessions
for (Session s : sandbox.process.listSessions()) {
System.out.println("Session: " + s.getSessionId() + ", Commands: " + s.getCommands());
}
```
```bash
# Get session info
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}'
# List all sessions
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session'
```
### Entrypoint session
Daytona provides methods to retrieve information about the internal entrypoint session in sandboxes. In each sandbox, the configured entrypoint command is executed inside a dedicated internal session, and you can fetch the session details (including the commands) and read its logs.
```python
# Entrypoint session details
session = sandbox.process.get_entrypoint_session()
print(f"Entrypoint session: {session.session_id}")
cmd = session.commands[0]
print(f"Entrypoint command id: {cmd.id}")
print(f"Command: {cmd.command}")
# Entrypoint logs (HTTP)
logs = sandbox.process.get_entrypoint_logs()
print(f"[STDOUT]: {logs.stdout}")
print(f"[STDERR]: {logs.stderr}")
# Stream entrypoint logs (WebSocket)
async def stream_entrypoint_logs():
await sandbox.process.get_entrypoint_logs_async(
lambda log: print(f"[STDOUT]: {log}"),
lambda log: print(f"[STDERR]: {log}"),
)
# Use asyncio.run in scripts; in notebooks or async apps, await stream_entrypoint_logs() instead.
asyncio.run(stream_entrypoint_logs())
```
```typescript
// Entrypoint session details
const session = await sandbox.process.getEntrypointSession();
console.log(`Entrypoint session: ${session.sessionId}`);
const cmd = session.commands[0]
console.log(`Entrypoint command id: ${cmd.id}`);
console.log(`Command: ${cmd.command}`);
// Entrypoint logs (HTTP)
const logs = await sandbox.process.getEntrypointLogs();
console.log('[STDOUT]:', logs.stdout);
console.log('[STDERR]:', logs.stderr);
// Stream entrypoint logs (WebSocket)
await sandbox.process.getEntrypointLogs(
(chunk) => console.log('[STDOUT]:', chunk),
(chunk) => console.log('[STDERR]:', chunk),
);
```
```ruby
# Entrypoint session details
session = sandbox.process.get_entrypoint_session
puts "Entrypoint session: #{session.session_id}"
cmd = session.commands.first
puts "Entrypoint command id: #{cmd.id}"
puts "Command: #{cmd.command}"
# Entrypoint logs (HTTP)
logs = sandbox.process.get_entrypoint_logs
puts "[STDOUT]: #{logs.stdout}"
puts "[STDERR]: #{logs.stderr}"
# Stream entrypoint logs (WebSocket)
sandbox.process.get_entrypoint_logs_async(
on_stdout: ->(log) { puts "[STDOUT]: #{log}" },
on_stderr: ->(log) { puts "[STDERR]: #{log}" }
)
```
```go
// Entrypoint session details
info, err := sandbox.Process.GetEntrypointSession(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Entrypoint session: %s\n", info.GetSessionId())
cmds := info.GetCommands()
cmd := cmds[0]
fmt.Printf("Entrypoint command id: %s\n", cmd.GetId())
fmt.Printf("Command: %s\n", cmd.GetCommand())
// Entrypoint logs (HTTP)
logs, err := sandbox.Process.GetEntrypointLogs(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(logs)
// Stream entrypoint logs (WebSocket)
stdout := make(chan string, 100)
stderr := make(chan string, 100)
go func() {
for msg := range stderr {
log.Printf("[STDERR]: %s", msg)
}
}()
go func() {
if err := sandbox.Process.GetEntrypointLogsStream(ctx, stdout, stderr); err != nil {
log.Println("Entrypoint log stream error:", err)
}
}()
for msg := range stdout {
fmt.Printf("[STDOUT]: %s\n", msg)
}
```
```java
import io.daytona.sdk.model.Command;
import io.daytona.sdk.model.Session;
import io.daytona.sdk.model.SessionCommandLogsResponse;
// Entrypoint session details
Session session = sandbox.process.getEntrypointSession();
System.out.println("Entrypoint session: " + session.getSessionId());
Command cmd = session.getCommands().get(0);
System.out.println("Entrypoint command id: " + cmd.getId());
System.out.println("Command: " + cmd.getCommand());
// Entrypoint logs (HTTP)
SessionCommandLogsResponse logs = sandbox.process.getEntrypointLogs();
System.out.println("[STDOUT]: " + logs.getStdout());
System.out.println("[STDERR]: " + logs.getStderr());
// Stream entrypoint logs (WebSocket)
sandbox.process.getEntrypointLogs(
chunk -> System.out.println("[STDOUT]: " + chunk),
chunk -> System.out.println("[STDERR]: " + chunk)
);
```
```bash
# Get entrypoint session details
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint'
# Get entrypoint logs (HTTP)
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint/logs'
# Follow entrypoint logs in real-time (WebSocket)
# wss://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint/logs?follow=true
```
### Execute interactive commands
Daytona provides methods to execute interactive commands in sessions. You can send input to running commands that expect user interaction, such as confirmations or interactive tools like database CLIs and package managers.
```python
session_id = "interactive-session"
sandbox.process.create_session(session_id)
# Execute command that requires confirmation
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='pip uninstall requests',
run_async=True,
),
)
# Stream logs asynchronously
logs_task = asyncio.create_task(
sandbox.process.get_session_command_logs_async(
session_id,
command.cmd_id,
lambda log: print(f"[STDOUT]: {log}"),
lambda log: print(f"[STDERR]: {log}"),
)
)
await asyncio.sleep(1)
# Send input to the command
sandbox.process.send_session_command_input(session_id, command.cmd_id, "y")
# Wait for logs to complete
await logs_task
```
```typescript
const sessionId = 'interactive-session'
await sandbox.process.createSession(sessionId)
// Execute command that requires confirmation
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: 'pip uninstall requests',
runAsync: true,
})
// Stream logs asynchronously
const logPromise = sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId!,
(stdout) => console.log('[STDOUT]:', stdout),
(stderr) => console.log('[STDERR]:', stderr),
)
await new Promise((resolve) => setTimeout(resolve, 1000))
// Send input to the command
await sandbox.process.sendSessionCommandInput(sessionId, command.cmdId!, 'y')
// Wait for logs to complete
await logPromise
```
```ruby
session_id = "interactive-session"
sandbox.process.create_session(session_id)
# Execute command that requires confirmation
interactive_command = sandbox.process.execute_session_command(
session_id: session_id,
req: Daytona::SessionExecuteRequest.new(
command: 'pip uninstall requests',
run_async: true
)
)
# Wait a moment for the command to start
sleep 1
# Send input to the command
sandbox.process.send_session_command_input(
session_id: session_id,
command_id: interactive_command.cmd_id,
data: "y"
)
# Get logs for the interactive command asynchronously
sandbox.process.get_session_command_logs_async(
session_id: session_id,
command_id: interactive_command.cmd_id,
on_stdout: ->(log) { puts "[STDOUT]: #{log}" },
on_stderr: ->(log) { puts "[STDERR]: #{log}" }
)
```
```go
sessionID := "interactive-session"
err := sandbox.Process.CreateSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
// Execute command that requires confirmation
result, err := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, "pip uninstall requests", true)
if err != nil {
log.Fatal(err)
}
cmdID := result["cmdId"].(string)
// Stream logs asynchronously
stdout := make(chan string)
stderr := make(chan string)
go func() {
err := sandbox.Process.GetSessionCommandLogsStream(ctx, sessionID, cmdID, stdout, stderr)
if err != nil {
log.Println("Log stream error:", err)
}
}()
time.Sleep(1 * time.Second)
// Note: SendSessionCommandInput is not available in Go SDK
// Use the API endpoint directly for sending input
// Read logs
for msg := range stdout {
fmt.Printf("[STDOUT]: %s\n", msg)
}
```
```java
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
String sessionId = "interactive-session";
sandbox.process.createSession(sessionId);
SessionExecuteResponse command = sandbox.process.executeSessionCommand(
sessionId,
new SessionExecuteRequest("pip uninstall requests", true)
);
String cmdId = command.getCmdId();
Thread logThread = new Thread(() -> sandbox.process.getSessionCommandLogs(
sessionId,
cmdId,
log -> System.out.println("[STDOUT]: " + log),
log -> System.out.println("[STDERR]: " + log)
));
logThread.start();
Thread.sleep(1000);
sandbox.process.sendSessionCommandInput(sessionId, cmdId, "y");
logThread.join();
```
```bash
# Create session
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"sessionId": "interactive-session"}'
# Execute session command
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/exec' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "pip uninstall requests",
"runAsync": true
}'
# Send input to command
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/input' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"data": "y"
}'
# Get command logs
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs'
```
## Resource management
Daytona provides methods to manage session resources. You should use sessions for long-running operations, clean up sessions after execution, and handle exceptions properly.
```python
# Python - Clean up session
session_id = "long-running-cmd"
try:
sandbox.process.create_session(session_id)
session = sandbox.process.get_session(session_id)
# Do work...
finally:
sandbox.process.delete_session(session.session_id)
```
```typescript
// TypeScript - Clean up session
const sessionId = "long-running-cmd";
try {
await sandbox.process.createSession(sessionId);
const session = await sandbox.process.getSession(sessionId);
// Do work...
} finally {
await sandbox.process.deleteSession(session.sessionId);
}
```
```ruby
# Ruby - Clean up session
session_id = 'long-running-cmd'
begin
sandbox.process.create_session(session_id)
session = sandbox.process.get_session(session_id)
# Do work...
ensure
sandbox.process.delete_session(session.session_id)
end
```
```go
// Go - Clean up session
sessionID := "long-running-cmd"
err := sandbox.Process.CreateSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
defer sandbox.Process.DeleteSession(ctx, sessionID)
session, err := sandbox.Process.GetSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
// Do work...
```
```java
import io.daytona.sdk.model.Session;
// Clean up session
String sessionId = "long-running-cmd";
try {
sandbox.process.createSession(sessionId);
Session session = sandbox.process.getSession(sessionId);
// Do work...
} finally {
sandbox.process.deleteSession(sessionId);
}
```
```bash
# Create session
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"sessionId": "long-running-cmd"}'
# Delete session when done
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}' \
--request DELETE
```
## Error handling
Daytona provides methods to handle errors when executing processes. You should handle process exceptions properly, log error details for debugging, and use try-catch blocks for error handling.
```python
from daytona import DaytonaError
try:
response = sandbox.process.code_run("invalid python code")
if response.exit_code != 0:
print(f"Exit code: {response.exit_code}")
print(f"Error output: {response.result}")
except DaytonaError as e:
print(f"Execution failed: {e}")
```
```typescript
import { DaytonaError } from '@daytona/sdk'
try {
const response = await sandbox.process.codeRun("invalid typescript code");
if (response.exitCode !== 0) {
console.error("Exit code:", response.exitCode);
console.error("Error output:", response.result);
}
} catch (e) {
if (e instanceof DaytonaError) {
console.error("Execution failed:", e);
}
}
```
```ruby
begin
response = sandbox.process.code_run(code: 'invalid python code')
if response.exit_code != 0
puts "Exit code: #{response.exit_code}"
puts "Error output: #{response.result}"
end
rescue StandardError => e
puts "Execution failed: #{e}"
end
```
```go
result, err := sandbox.Process.ExecuteCommand(ctx, "python3 -c 'invalid python code'")
if err != nil {
fmt.Println("Execution failed:", err)
}
if result != nil && result.ExitCode != 0 {
fmt.Println("Exit code:", result.ExitCode)
fmt.Println("Error output:", result.Result)
}
```
```java
import io.daytona.sdk.exception.DaytonaException;
import io.daytona.sdk.model.ExecuteResponse;
try {
ExecuteResponse response = sandbox.process.codeRun("invalid python code");
if (response.getExitCode() != null && response.getExitCode() != 0) {
System.out.println("Exit code: " + response.getExitCode());
System.out.println("Error output: " + response.getResult());
}
} catch (DaytonaException e) {
System.out.println("Execution failed: " + e.getMessage());
}
```
```bash
# API responses include exitCode field for error handling
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "python3 -c \"invalid python code\""
}'
# Response includes:
# {
# "result": "",
# "exitCode": 1
# }
```
## Common issues
Daytona provides solutions for troubleshooting common issues related to process and code execution.
| **Issue** | **Solutions** |
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Process execution failed | • Check command syntax • Verify required dependencies • Ensure sufficient permissions |
| Process timeout | • Adjust timeout settings • Optimize long-running operations • Consider using background processes |
| Resource limits | • Monitor process memory usage • Handle process cleanup properly • Use appropriate resource constraints |
# Pseudo Terminal (PTY)
Daytona provides powerful pseudo terminal (PTY) capabilities through the `process` module in sandboxes. PTY sessions allow you to create interactive terminal sessions that can execute commands, handle user input, and manage terminal operations.
A PTY (Pseudo Terminal) is a virtual terminal interface that allows programs to interact with a shell as if they were connected to a real terminal. PTY sessions in Daytona enable:
- **Interactive Development**: REPLs, debuggers, and development tools
- **Build Processes**: Running and monitoring compilation, testing, or deployment
- **System Administration**: Remote server management and configuration
- **User Interfaces**: Terminal-based applications requiring user interaction
## Create PTY session
Daytona provides methods to create an interactive terminal session that can execute commands and handle user input.
```python
from daytona.common.pty import PtySize
pty_handle = sandbox.process.create_pty_session(
id="my-session",
cwd="/workspace",
envs={"TERM": "xterm-256color"},
pty_size=PtySize(cols=120, rows=30)
)
```
```typescript
// Create a PTY session with custom configuration
const ptyHandle = await sandbox.process.createPty({
id: 'my-interactive-session',
cwd: '/workspace',
envs: { TERM: 'xterm-256color', LANG: 'en_US.UTF-8' },
cols: 120,
rows: 30,
onData: (data) => {
// Handle terminal output
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
// Wait for connection to be established
await ptyHandle.waitForConnection()
// Send commands to the terminal
await ptyHandle.sendInput('ls -la\n')
await ptyHandle.sendInput('echo "Hello, PTY!"\n')
await ptyHandle.sendInput('exit\n')
// Wait for completion and get result
const result = await ptyHandle.wait()
console.log(`PTY session completed with exit code: ${result.exitCode}`)
// Clean up
await ptyHandle.disconnect()
```
```ruby
pty_size = Daytona::PtySize.new(rows: 30, cols: 120)
pty_handle = sandbox.process.create_pty_session(
id: 'my-interactive-session',
cwd: '/workspace',
envs: { 'TERM' => 'xterm-256color' },
pty_size: pty_size
)
# Use the PTY session
pty_handle.send_input("ls -la\n")
pty_handle.send_input("echo 'Hello, PTY!'\n")
pty_handle.send_input("exit\n")
# Handle output
pty_handle.each { |data| print data }
puts "PTY session completed with exit code: #{pty_handle.exit_code}"
```
```go
// Create a PTY session with custom configuration
handle, err := sandbox.Process.CreatePty(ctx, "my-interactive-session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
options.WithCreatePtyEnv(map[string]string{"TERM": "xterm-256color"}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Send commands to the terminal
handle.SendInput([]byte("ls -la\n"))
handle.SendInput([]byte("echo 'Hello, PTY!'\n"))
handle.SendInput([]byte("exit\n"))
// Read output from channel
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Wait for completion and get result
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("PTY session completed with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("my-interactive-session")
.setCols(120)
.setRows(30)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle ptyHandle = sandbox.process.createPty(options);
ptyHandle.waitForConnection(30);
ptyHandle.sendInput("ls -la\n");
ptyHandle.sendInput("echo \"Hello, PTY!\"\n");
ptyHandle.sendInput("exit\n");
PtyResult result = ptyHandle.waitForExit();
System.out.println("PTY session completed with exit code: " + result.getExitCode());
ptyHandle.disconnect();
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"cols": 1,
"cwd": "",
"envs": {
"additionalProperty": ""
},
"id": "",
"lazyStart": true,
"rows": 1
}'
```
## Connect to PTY session
Daytona provides methods to establish a connection to an existing PTY session, enabling interaction with a previously created terminal.
```python
pty_handle = sandbox.process.connect_pty_session("my-session")
```
```typescript
// Connect to an existing PTY session
const handle = await sandbox.process.connectPty('my-session', {
onData: (data) => {
// Handle terminal output
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
// Wait for connection to be established
await handle.waitForConnection()
// Send commands to the existing session
await handle.sendInput('pwd\n')
await handle.sendInput('ls -la\n')
await handle.sendInput('exit\n')
// Wait for completion
const result = await handle.wait()
console.log(`Session exited with code: ${result.exitCode}`)
// Clean up
await handle.disconnect()
```
```ruby
# Connect to an existing PTY session
pty_handle = sandbox.process.connect_pty_session('my-session')
pty_handle.send_input("echo 'Hello World'\n")
pty_handle.send_input("exit\n")
# Handle output
pty_handle.each { |data| print data }
puts "Session exited with code: #{pty_handle.exit_code}"
```
```go
// Connect to an existing PTY session
handle, err := sandbox.Process.ConnectPty(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Send commands to the existing session
handle.SendInput([]byte("pwd\n"))
handle.SendInput([]byte("ls -la\n"))
handle.SendInput([]byte("exit\n"))
// Read output
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Wait for completion
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session exited with code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.connectPty("my-session", options);
handle.waitForConnection(30);
handle.sendInput("pwd\n");
handle.sendInput("ls -la\n");
handle.sendInput("exit\n");
PtyResult result = handle.waitForExit();
System.out.println("Session exited with code: " + result.getExitCode());
handle.disconnect();
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}/connect'
```
## List PTY sessions
Daytona provides methods to list PTY sessions, allowing you to retrieve information about all PTY sessions, both active and inactive, that have been created in the sandbox.
```python
# List all PTY sessions
sessions = sandbox.process.list_pty_sessions()
for session in sessions:
print(f"Session ID: {session.id}")
print(f"Active: {session.active}")
print(f"Created: {session.created_at}")
```
```typescript
// List all PTY sessions
const sessions = await sandbox.process.listPtySessions()
for (const session of sessions) {
console.log(`Session ID: ${session.id}`)
console.log(`Active: ${session.active}`)
console.log(`Created: ${session.createdAt}`)
console.log('---')
}
```
```ruby
# List all PTY sessions
sessions = sandbox.process.list_pty_sessions
sessions.each do |session|
puts "Session ID: #{session.id}"
puts "Active: #{session.active}"
puts "Terminal Size: #{session.cols}x#{session.rows}"
puts '---'
end
```
```go
// List all PTY sessions
sessions, err := sandbox.Process.ListPtySessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, session := range sessions {
fmt.Printf("Session ID: %s\n", session.Id)
fmt.Printf("Active: %t\n", session.Active)
fmt.Printf("Terminal Size: %dx%d\n", session.Cols, session.Rows)
fmt.Println("---")
}
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
import java.util.List;
List sessions = sandbox.process.listPtySessions();
for (PtySessionInfo session : sessions) {
System.out.println("Session ID: " + session.getId());
System.out.println("Active: " + session.getActive());
System.out.println("Created: " + session.getCreatedAt());
System.out.println("---");
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty'
```
## Get PTY session info
Daytona provides methods to get information about a specific PTY session, allowing you to retrieve comprehensive information about a specific PTY session including its current state, configuration, and metadata.
```python
# Get details about a specific PTY session
session_info = sandbox.process.get_pty_session_info("my-session")
print(f"Session ID: {session_info.id}")
print(f"Active: {session_info.active}")
print(f"Working Directory: {session_info.cwd}")
print(f"Terminal Size: {session_info.cols}x{session_info.rows}")
```
```typescript
// Get details about a specific PTY session
const session = await sandbox.process.getPtySessionInfo('my-session')
console.log(`Session ID: ${session.id}`)
console.log(`Active: ${session.active}`)
console.log(`Working Directory: ${session.cwd}`)
console.log(`Terminal Size: ${session.cols}x${session.rows}`)
if (session.processId) {
console.log(`Process ID: ${session.processId}`)
}
```
```ruby
# Get details about a specific PTY session
session_info = sandbox.process.get_pty_session_info('my-session')
puts "Session ID: #{session_info.id}"
puts "Active: #{session_info.active}"
puts "Working Directory: #{session_info.cwd}"
puts "Terminal Size: #{session_info.cols}x#{session_info.rows}"
```
```go
// Get details about a specific PTY session
session, err := sandbox.Process.GetPtySessionInfo(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session ID: %s\n", session.Id)
fmt.Printf("Active: %t\n", session.Active)
fmt.Printf("Working Directory: %s\n", session.Cwd)
fmt.Printf("Terminal Size: %dx%d\n", session.Cols, session.Rows)
if session.ProcessId != nil {
fmt.Printf("Process ID: %d\n", *session.ProcessId)
}
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
PtySessionInfo session = sandbox.process.getPtySessionInfo("my-session");
System.out.println("Session ID: " + session.getId());
System.out.println("Active: " + session.getActive());
System.out.println("Working Directory: " + session.getCwd());
System.out.println("Terminal Size: " + session.getCols() + "x" + session.getRows());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}'
```
## Kill PTY session
Daytona provides methods to kill a PTY session, allowing you to forcefully terminate a PTY session and cleans up all associated resources.
```python
# Kill a specific PTY session
sandbox.process.kill_pty_session("my-session")
# Verify the session no longer exists
pty_sessions = sandbox.process.list_pty_sessions()
for pty_session in pty_sessions:
print(f"PTY session: {pty_session.id}")
```
```typescript
// Kill a specific PTY session
await sandbox.process.killPtySession('my-session')
// Verify the session is no longer active
try {
const info = await sandbox.process.getPtySessionInfo('my-session')
console.log(`Session still exists but active: ${info.active}`)
} catch (error) {
console.log('Session has been completely removed')
}
```
```ruby
# Delete a specific PTY session
sandbox.process.delete_pty_session('my-session')
# Verify the session no longer exists
sessions = sandbox.process.list_pty_sessions
sessions.each do |session|
puts "PTY session: #{session.id}"
end
```
```go
// Kill a specific PTY session
err := sandbox.Process.KillPtySession(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
// Verify the session is no longer active
sessions, err := sandbox.Process.ListPtySessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, session := range sessions {
fmt.Printf("PTY session: %s\n", session.Id)
}
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
import java.util.List;
sandbox.process.killPtySession("my-session");
List sessions = sandbox.process.listPtySessions();
for (PtySessionInfo session : sessions) {
System.out.println("PTY session: " + session.getId());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}' \
--request DELETE
```
## Resize PTY session
Daytona provides methods to resize a PTY session, allowing you to change the terminal dimensions of an active PTY session. This sends a SIGWINCH signal to the shell process, allowing terminal applications to adapt to the new size.
```python
from daytona.common.pty import PtySize
# Resize a PTY session to a larger terminal
new_size = PtySize(rows=40, cols=150)
updated_info = sandbox.process.resize_pty_session("my-session", new_size)
print(f"Terminal resized to {updated_info.cols}x{updated_info.rows}")
# You can also use the PtyHandle's resize method
pty_handle.resize(new_size)
```
```typescript
// Resize a PTY session to a larger terminal
const updatedInfo = await sandbox.process.resizePtySession('my-session', 150, 40)
console.log(`Terminal resized to ${updatedInfo.cols}x${updatedInfo.rows}`)
// You can also use the PtyHandle's resize method
await ptyHandle.resize(150, 40) // cols, rows
```
```ruby
# Resize a PTY session to a larger terminal
pty_size = Daytona::PtySize.new(rows: 40, cols: 150)
session_info = sandbox.process.resize_pty_session('my-session', pty_size)
puts "Terminal resized to #{session_info.cols}x#{session_info.rows}"
```
```go
// Resize a PTY session to a larger terminal
updatedInfo, err := sandbox.Process.ResizePtySession(ctx, "my-session", types.PtySize{
Cols: 150,
Rows: 40,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Terminal resized to %dx%d\n", updatedInfo.Cols, updatedInfo.Rows)
// You can also use the PtyHandle's Resize method
info, err := handle.Resize(ctx, 150, 40)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Terminal resized to %dx%d\n", info.Cols, info.Rows)
```
```java
sandbox.process.resizePtySession("my-session", 150, 40);
// Or resize using the handle (after createPty or connectPty)
handle.resize(150, 40);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}/resize' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"cols": 1,
"rows": 1
}'
```
## Interactive commands
Daytona provides methods to handle interactive commands with PTY sessions, allowing you to handle interactive commands that require user input and can be resized during execution.
```python
import time
from daytona import Daytona, Sandbox
from daytona.common.pty import PtySize
def handle_pty_data(data: bytes):
text = data.decode("utf-8", errors="replace")
print(text, end="")
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id="interactive-session",
pty_size=PtySize(cols=300, rows=100)
)
# Send interactive command
pty_handle.send_input('printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi\n')
time.sleep(1)
pty_handle.send_input("y\n")
# Resize terminal
pty_session_info = pty_handle.resize(PtySize(cols=210, rows=110))
print(f"PTY session resized to {pty_session_info.cols}x{pty_session_info.rows}")
# Exit the session
pty_handle.send_input('exit\n')
# Handle output using iterator
for data in pty_handle:
handle_pty_data(data)
print(f"Session completed with exit code: {pty_handle.exit_code}")
```
```typescript
import { Daytona, Sandbox } from '@daytona/sdk'
// Create PTY session
const ptyHandle = await sandbox.process.createPty({
id: 'interactive-session',
cols: 300,
rows: 100,
onData: data => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
await ptyHandle.waitForConnection()
// Send interactive command
await ptyHandle.sendInput(
'printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi\n'
)
await new Promise(resolve => setTimeout(resolve, 1000))
await ptyHandle.sendInput('y\n')
// Resize terminal
const ptySessionInfo = await sandbox.process.resizePtySession(
'interactive-session',
210,
110
)
console.log(
`\nPTY session resized to ${ptySessionInfo.cols}x${ptySessionInfo.rows}`
)
// Exit the session
await ptyHandle.sendInput('exit\n')
// Wait for completion
const result = await ptyHandle.wait()
console.log(`Session completed with exit code: ${result.exitCode}`)
```
```ruby
require 'daytona'
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id: 'interactive-session',
pty_size: Daytona::PtySize.new(cols: 300, rows: 100)
)
# Handle output in a separate thread
thread = Thread.new do
pty_handle.each { |data| print data }
end
# Send interactive command
pty_handle.send_input('printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi' + "\n")
sleep(1)
pty_handle.send_input("y\n")
# Resize terminal
pty_handle.resize(Daytona::PtySize.new(cols: 210, rows: 110))
puts "\nPTY session resized"
# Exit the session
pty_handle.send_input("exit\n")
# Wait for the thread to finish
thread.join
puts "Session completed with exit code: #{pty_handle.exit_code}"
```
```go
// Create PTY session
handle, err := sandbox.Process.CreatePty(ctx, "interactive-session",
options.WithCreatePtySize(types.PtySize{Cols: 300, Rows: 100}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Handle output in a goroutine
go func() {
for data := range handle.DataChan() {
fmt.Print(string(data))
}
}()
// Send interactive command
handle.SendInput([]byte(`printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi` + "\n"))
time.Sleep(1 * time.Second)
handle.SendInput([]byte("y\n"))
// Resize terminal
info, err := handle.Resize(ctx, 210, 110)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nPTY session resized to %dx%d\n", info.Cols, info.Rows)
// Exit the session
handle.SendInput([]byte("exit\n"))
// Wait for completion
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session completed with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("interactive-session")
.setCols(300)
.setRows(100)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.createPty(options);
handle.waitForConnection(30);
handle.sendInput(
"printf \"Are you accepting the terms and conditions? (y/n): \" && read confirm && if [ \"$confirm\" = \"y\" ]; then echo \"You accepted\"; else echo \"You did not accept\"; fi\n");
Thread.sleep(1000);
handle.sendInput("y\n");
sandbox.process.resizePtySession("interactive-session", 210, 110);
System.out.println("\nPTY session resized");
handle.sendInput("exit\n");
PtyResult result = handle.waitForExit();
System.out.println("Session completed with exit code: " + result.getExitCode());
```
## Long-running processes
Daytona provides methods to manage long-running processes with PTY sessions, allowing you to manage long-running processes that need to be monitored or terminated.
```python
import time
import threading
from daytona import Daytona, Sandbox
from daytona.common.pty import PtySize
def handle_pty_data(data: bytes):
text = data.decode("utf-8", errors="replace")
print(text, end="")
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id="long-running-session",
pty_size=PtySize(cols=120, rows=30)
)
# Start a long-running process
pty_handle.send_input('while true; do echo "Running... $(date)"; sleep 1; done\n')
# Using thread and wait() method to handle PTY output
thread = threading.Thread(target=pty_handle.wait, args=(handle_pty_data, 10))
thread.start()
time.sleep(3) # Let it run for a bit
print("Killing long-running process...")
pty_handle.kill()
thread.join()
print(f"\nProcess terminated with exit code: {pty_handle.exit_code}")
if pty_handle.error:
print(f"Termination reason: {pty_handle.error}")
```
```typescript
import { Daytona, Sandbox } from '@daytona/sdk'
// Create PTY session
const ptyHandle = await sandbox.process.createPty({
id: 'long-running-session',
cols: 120,
rows: 30,
onData: (data) => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
await ptyHandle.waitForConnection()
// Start a long-running process
await ptyHandle.sendInput('while true; do echo "Running... $(date)"; sleep 1; done\n')
await new Promise(resolve => setTimeout(resolve, 3000)) // Let it run for a bit
console.log('Killing long-running process...')
await ptyHandle.kill()
// Wait for termination
const result = await ptyHandle.wait()
console.log(`\nProcess terminated with exit code: ${result.exitCode}`)
if (result.error) {
console.log(`Termination reason: ${result.error}`)
}
```
```ruby
require 'daytona'
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id: 'long-running-session',
pty_size: Daytona::PtySize.new(cols: 120, rows: 30)
)
# Handle output in a separate thread
thread = Thread.new do
pty_handle.each { |data| print data }
end
# Start a long-running process
pty_handle.send_input("while true; do echo \"Running... $(date)\"; sleep 1; done\n")
sleep(3) # Let it run for a bit
puts "Killing long-running process..."
pty_handle.kill
thread.join
puts "\nProcess terminated with exit code: #{pty_handle.exit_code}"
puts "Termination reason: #{pty_handle.error}" if pty_handle.error
```
```go
// Create PTY session
handle, err := sandbox.Process.CreatePty(ctx, "long-running-session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Handle output in a goroutine
go func() {
for data := range handle.DataChan() {
fmt.Print(string(data))
}
}()
// Start a long-running process
handle.SendInput([]byte(`while true; do echo "Running... $(date)"; sleep 1; done` + "\n"))
time.Sleep(3 * time.Second) // Let it run for a bit
fmt.Println("Killing long-running process...")
if err := handle.Kill(ctx); err != nil {
log.Fatal(err)
}
// Wait for termination
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nProcess terminated with exit code: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Termination reason: %s\n", *result.Error)
}
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("long-running-session")
.setCols(120)
.setRows(30)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.createPty(options);
handle.waitForConnection(30);
handle.sendInput("while true; do echo \"Running... $(date)\"; sleep 1; done\n");
Thread.sleep(3000);
System.out.println("Killing long-running process...");
handle.kill();
PtyResult result = handle.waitForExit();
System.out.println("\nProcess terminated with exit code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Termination reason: " + result.getError());
}
```
## Resource management
Daytona provides methods to manage resource leaks with PTY sessions, allowing you to always clean up PTY sessions to prevent resource leaks.
```python
# Python: Use try/finally
pty_handle = None
try:
pty_handle = sandbox.process.create_pty_session(id="session", pty_size=PtySize(cols=120, rows=30))
# Do work...
finally:
if pty_handle:
pty_handle.kill()
```
```typescript
// TypeScript: Use try/finally
let ptyHandle
try {
ptyHandle = await sandbox.process.createPty({
id: 'session',
cols: 120,
rows: 30,
})
// Do work...
} finally {
if (ptyHandle) await ptyHandle.kill()
}
```
```ruby
# Ruby: Use begin/ensure
pty_handle = nil
begin
pty_handle = sandbox.process.create_pty_session(
id: 'session',
pty_size: Daytona::PtySize.new(cols: 120, rows: 30)
)
# Do work...
ensure
pty_handle&.kill
end
```
```go
// Go: Use defer for cleanup
handle, err := sandbox.Process.CreatePty(ctx, "session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Do work...
// Or use Kill to terminate the process
defer handle.Kill(ctx)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
PtyHandle handle = null;
try {
handle = sandbox.process.createPty(
new PtyCreateOptions().setId("session").setCols(120).setRows(30));
handle.waitForConnection(30);
// Do work...
} finally {
if (handle != null) {
handle.kill();
handle.disconnect();
}
}
```
## PtyHandle methods
Daytona provides methods to interact with PTY sessions, allowing you to send input, resize the terminal, wait for completion, and manage the WebSocket connection to a PTY session.
### Send input
Daytona provides methods to send input to a PTY session, allowing you to send input data (keystrokes or commands) to the PTY session.
```python
# Send a command
pty_handle.send_input("ls -la\n")
# Send user input
pty_handle.send_input("y\n")
```
```typescript
// Send a command
await ptyHandle.sendInput('ls -la\n')
// Send raw bytes
await ptyHandle.sendInput(new Uint8Array([3])) // Ctrl+C
```
```ruby
# Send a command
pty_handle.send_input("ls -la\n")
# Send user input
pty_handle.send_input("y\n")
```
```go
// Send a command
handle.SendInput([]byte("ls -la\n"))
// Send Ctrl+C
handle.SendInput([]byte{0x03})
```
```java
// Send a command
ptyHandle.sendInput("ls -la\n");
// Send raw bytes (Ctrl+C)
ptyHandle.sendInput(new byte[] { 0x03 });
```
### Wait for completion
Daytona provides methods to wait for a PTY process to exit and return the result, allowing you to wait for a PTY process to exit and return the result.
```python
# Wait with a callback for output data
def handle_data(data: bytes):
print(data.decode("utf-8", errors="replace"), end="")
result = pty_handle.wait(on_data=handle_data, timeout=30)
print(f"Exit code: {result.exit_code}")
```
```typescript
// Wait for process to complete
const result = await ptyHandle.wait()
if (result.exitCode === 0) {
console.log('Process completed successfully')
} else {
console.log(`Process failed with code: ${result.exitCode}`)
if (result.error) {
console.log(`Error: ${result.error}`)
}
}
```
```ruby
# Wait by iterating over output (blocks until PTY session ends)
pty_handle.each { |data| print data }
if pty_handle.exit_code == 0
puts 'Process completed successfully'
else
puts "Process failed with code: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}" if pty_handle.error
end
```
```go
// Wait for process to complete
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
if result.ExitCode != nil && *result.ExitCode == 0 {
fmt.Println("Process completed successfully")
} else {
fmt.Printf("Process failed with code: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Error: %s\n", *result.Error)
}
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
if (result.getExitCode() == 0) {
System.out.println("Process completed successfully");
} else {
System.out.println("Process failed with code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
}
```
### Wait for connection
Daytona provides methods to wait for the WebSocket connection to be established before sending input, allowing you to wait for the WebSocket connection to be established before sending input.
```python
# Python handles connection internally during creation
# No explicit wait needed
```
```typescript
// Wait for connection to be established
await ptyHandle.waitForConnection()
// Now safe to send input
await ptyHandle.sendInput('echo "Connected!"\n')
```
```ruby
# Ruby handles connection internally during creation
# No explicit wait needed - can send input immediately after creation
pty_handle.send_input("echo 'Connected!'\n")
```
```go
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Now safe to send input
handle.SendInput([]byte("echo 'Connected!'\n"))
```
```java
// Wait for connection (timeout in seconds)
ptyHandle.waitForConnection(30);
ptyHandle.sendInput("echo \"Connected!\"\n");
```
### Kill PTY process
Daytona provides methods to kill a PTY process and terminate the session from the handle.
```python
pty_handle.kill()
```
```typescript
// Kill a long-running process
await ptyHandle.kill()
// Wait to confirm termination
const result = await ptyHandle.wait()
console.log(`Process terminated with exit code: ${result.exitCode}`)
```
```ruby
# Kill a long-running process
pty_handle.kill
puts "Process terminated with exit code: #{pty_handle.exit_code}"
```
```go
// Kill a long-running process
if err := handle.Kill(ctx); err != nil {
log.Fatal(err)
}
// Wait to confirm termination
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Process terminated with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyResult;
ptyHandle.kill();
PtyResult result = ptyHandle.waitForExit();
System.out.println("Process terminated with exit code: " + result.getExitCode());
```
### Resize from handle
Daytona provides methods to resize the PTY terminal dimensions directly from the handle.
```python
from daytona.common.pty import PtySize
pty_handle.resize(PtySize(cols=120, rows=30))
```
```typescript
// Resize to 120x30
await ptyHandle.resize(120, 30)
```
```ruby
# Resize to 120x30
pty_handle.resize(Daytona::PtySize.new(cols: 120, rows: 30))
```
```go
// Resize to 120x30
info, err := handle.Resize(ctx, 120, 30)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resized to %dx%d\n", info.Cols, info.Rows)
```
```java
// Resize to 120x30
ptyHandle.resize(120, 30);
```
### Disconnect
Daytona provides methods to disconnect from a PTY session and clean up resources without killing the process.
```python
# Python: Use kill() to terminate, or let the handle go out of scope
```
```typescript
// Always clean up when done
try {
// ... use PTY session
} finally {
await ptyHandle.disconnect()
}
```
```ruby
# Ruby: Use begin/ensure or kill the session
begin
# ... use PTY session
ensure
pty_handle.kill
end
```
```go
// Always clean up when done using defer
handle, err := sandbox.Process.CreatePty(ctx, "session")
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// ... use PTY session
```
```java
try {
// ... use PTY session
} finally {
ptyHandle.disconnect();
}
```
### Check connection status
Daytona provides methods to check if a PTY session is still connected.
```python
# Python: Check by attempting operations or using session info
session_info = sandbox.process.get_pty_session_info("my-session")
print(f"Session active: {session_info.active}")
```
```typescript
if (ptyHandle.isConnected()) {
console.log('PTY session is active')
}
```
```ruby
# Ruby: Check by using session info
session_info = sandbox.process.get_pty_session_info('my-session')
puts 'PTY session is active' if session_info.active
```
```go
if handle.IsConnected() {
fmt.Println("PTY session is active")
}
```
```java
if (ptyHandle.isConnected()) {
System.out.println("PTY session is active");
}
```
### Exit code and error
Daytona provides methods to access the exit code and error message after a PTY process terminates.
```python
# After iteration or wait completes
print(f"Exit code: {pty_handle.exit_code}")
if pty_handle.error:
print(f"Error: {pty_handle.error}")
```
```typescript
// Access via getters after process terminates
console.log(`Exit code: ${ptyHandle.exitCode}`)
if (ptyHandle.error) {
console.log(`Error: ${ptyHandle.error}`)
}
```
```ruby
# Access after process terminates
puts "Exit code: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}" if pty_handle.error
```
```go
// Access via methods after process terminates
if exitCode := handle.ExitCode(); exitCode != nil {
fmt.Printf("Exit code: %d\n", *exitCode)
}
if errMsg := handle.Error(); errMsg != nil {
fmt.Printf("Error: %s\n", *errMsg)
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
System.out.println("Exit code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
```
### Iterate over output (Python)
Daytona provides methods to iterate over a PTY handle to receive output data.
```python
# Iterate over PTY output
for data in pty_handle:
text = data.decode("utf-8", errors="replace")
print(text, end="")
print(f"Session ended with exit code: {pty_handle.exit_code}")
```
```typescript
// TypeScript uses the onData callback instead
const ptyHandle = await sandbox.process.createPty({
id: 'my-session',
onData: (data) => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
```
```ruby
# Iterate over PTY output
pty_handle.each do |data|
print data
end
puts "Session ended with exit code: #{pty_handle.exit_code}"
```
```go
// Go uses a channel to receive output data
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Or use as io.Reader
io.Copy(os.Stdout, handle)
fmt.Printf("Session ended with exit code: %d\n", *handle.ExitCode())
```
```java
import io.daytona.sdk.PtyCreateOptions;
import java.nio.charset.StandardCharsets;
PtyHandle ptyHandle = sandbox.process.createPty(
new PtyCreateOptions()
.setId("my-session")
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8))));
```
## Error handling
Daytona provides methods to monitor exit codes and handle errors appropriately with PTY sessions.
```python
# Python: Check exit codes
result = pty_handle.wait()
if result.exit_code != 0:
print(f"Command failed: {result.exit_code}")
print(f"Error: {result.error}")
```
```typescript
// TypeScript: Check exit codes
const result = await ptyHandle.wait()
if (result.exitCode !== 0) {
console.log(`Command failed: ${result.exitCode}`)
console.log(`Error: ${result.error}`)
}
```
```ruby
# Ruby: Check exit codes
# The handle blocks until the PTY session completes
pty_handle.each { |data| print data }
if pty_handle.exit_code != 0
puts "Command failed: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}"
end
```
```go
// Go: Check exit codes
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
if result.ExitCode != nil && *result.ExitCode != 0 {
fmt.Printf("Command failed: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Error: %s\n", *result.Error)
}
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
if (result.getExitCode() != 0) {
System.out.println("Command failed: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
}
```
## Troubleshooting
- **Connection issues**: verify sandbox status, network connectivity, and proper session IDs
- **Performance issues**: use appropriate terminal dimensions and efficient data handlers
- **Process management**: use explicit `kill()` calls and proper timeout handling for long-running processes
# Log Streaming
Log streaming allows you to access and process logs as they are being produced, while the process is still running. When executing long-running processes in a sandbox, you often want to access and process their logs in **real-time**.
Real-time streaming is especially useful for **debugging**, **monitoring**, or integrating with **observability tools**.
- [**Log streaming**](#stream-logs-with-callbacks): stream logs as they are being produced, while the process is still running.
- [**Fetching log snapshot**](#retrieve-all-existing-logs): retrieve all logs up to a certain point.
This guide covers how to use log streaming with callbacks and fetching log snapshots in both asynchronous and synchronous modes.
:::note
Starting with version `0.27.0`, you can retrieve session command logs in two distinct streams: **stdout** and **stderr**.
:::
## Stream logs with callbacks
If your sandboxed process is part of a larger system and is expected to run for an extended period (or indefinitely),
you can process logs asynchronously **in the background**, while the rest of your system continues executing.
This is ideal for:
- Continuous monitoring
- Debugging long-running jobs
- Live log forwarding or visualizations
```python
import asyncio
from daytona import Daytona, SessionExecuteRequest
async def main():
daytona = Daytona()
sandbox = daytona.create()
session_id = "streaming-session"
sandbox.process.create_session(session_id)
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
var_async=True,
),
)
# Stream logs with separate callbacks
logs_task = asyncio.create_task(
sandbox.process.get_session_command_logs_async(
session_id,
command.cmd_id,
lambda stdout: print(f"[STDOUT]: {stdout}"),
lambda stderr: print(f"[STDERR]: {stderr}"),
)
)
print("Continuing execution while logs are streaming...")
await asyncio.sleep(3)
print("Other operations completed!")
# Wait for the logs to complete
await logs_task
sandbox.delete()
if __name__ == "__main__":
asyncio.run(main())
```
```typescript
import { Daytona, SessionExecuteRequest } from '@daytona/sdk'
async function main() {
const daytona = new Daytona()
const sandbox = await daytona.create()
const sessionId = "exec-session-1"
await sandbox.process.createSession(sessionId)
const command = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
runAsync: true,
},
)
// Stream logs with separate callbacks
const logsTask = sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId!,
(stdout) => console.log('[STDOUT]:', stdout),
(stderr) => console.log('[STDERR]:', stderr),
)
console.log('Continuing execution while logs are streaming...')
await new Promise((resolve) => setTimeout(resolve, 3000))
console.log('Other operations completed!')
// Wait for the logs to complete
await logsTask
await sandbox.delete()
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
session_id = 'streaming-session'
sandbox.process.create_session(session_id)
command = sandbox.process.execute_session_command(
session_id,
Daytona::SessionExecuteRequest.new(
command: 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
var_async: true
)
)
# Stream logs using a thread
log_thread = Thread.new do
sandbox.process.get_session_command_logs_stream(
session_id,
command.cmd_id,
on_stdout: ->(stdout) { puts "[STDOUT]: #{stdout}" },
on_stderr: ->(stderr) { puts "[STDERR]: #{stderr}" }
)
end
puts 'Continuing execution while logs are streaming...'
sleep(3)
puts 'Other operations completed!'
# Wait for the logs to complete
log_thread.join
daytona.delete(sandbox)
```
```go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
sessionID := "streaming-session"
sandbox.Process.CreateSession(ctx, sessionID)
// Execute async command that outputs to stdout and stderr
cmd := `for i in 1 2 3 4 5; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done`
cmdResult, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, cmd, true)
cmdID, _ := cmdResult["id"].(string)
// Create channels for stdout and stderr
stdout := make(chan string, 100)
stderr := make(chan string, 100)
// Stream logs in a goroutine
go func() {
err := sandbox.Process.GetSessionCommandLogsStream(ctx, sessionID, cmdID, stdout, stderr)
if err != nil {
log.Printf("Stream error: %v", err)
}
}()
fmt.Println("Continuing execution while logs are streaming...")
// Read from channels until both are closed
stdoutOpen, stderrOpen := true, true
for stdoutOpen || stderrOpen {
select {
case chunk, ok := <-stdout:
if !ok {
stdoutOpen = false
} else {
fmt.Fprintf(os.Stdout, "[STDOUT]: %s", chunk)
}
case chunk, ok := <-stderr:
if !ok {
stderrOpen = false
} else {
fmt.Fprintf(os.Stderr, "[STDERR]: %s", chunk)
}
}
}
fmt.Println("Streaming completed!")
sandbox.Delete(ctx)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
public class App {
public static void main(String[] args) throws InterruptedException {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
String sessionId = "streaming-session";
sandbox.getProcess().createSession(sessionId);
SessionExecuteResponse command = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"for i in {1..5}; do echo \"Step $i\"; echo \"Error $i\" >&2; sleep 1; done",
true));
Thread logThread = new Thread(() -> sandbox.getProcess().getSessionCommandLogs(
sessionId,
command.getCmdId(),
stdout -> System.out.print("[STDOUT]: " + stdout),
stderr -> System.err.print("[STDERR]: " + stderr)));
logThread.start();
System.out.println("Continuing execution while logs are streaming...");
Thread.sleep(3000);
System.out.println("Other operations completed!");
logThread.join();
sandbox.delete();
}
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs'
```
## Retrieve all existing logs
If the command has a predictable duration, or if you don't need to run it in the background but want to
periodically check all existing logs, you can use the following example to get the logs up to the current point in time.
```python
import time
from daytona import Daytona, SessionExecuteRequest
daytona = Daytona()
sandbox = daytona.create()
session_id = "exec-session-1"
sandbox.process.create_session(session_id)
# Execute a blocking command and wait for the result
command = sandbox.process.execute_session_command(
session_id, SessionExecuteRequest(command="echo 'Hello from stdout' && echo 'Hello from stderr' >&2")
)
print(f"[STDOUT]: {command.stdout}")
print(f"[STDERR]: {command.stderr}")
print(f"[OUTPUT]: {command.output}")
# Or execute command in the background and get the logs later
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
run_async=True
)
)
time.sleep(5)
# Get the logs up to the current point in time
logs = sandbox.process.get_session_command_logs(session_id, command.cmd_id)
print(f"[STDOUT]: {logs.stdout}")
print(f"[STDERR]: {logs.stderr}")
print(f"[OUTPUT]: {logs.output}")
sandbox.delete()
```
```typescript
import { Daytona, SessionExecuteRequest } from '@daytona/sdk'
async function main() {
const daytona = new Daytona()
const sandbox = await daytona.create()
const sessionId = "exec-session-1"
await sandbox.process.createSession(sessionId)
// Execute a blocking command and wait for the result
const command = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'echo "Hello from stdout" && echo "Hello from stderr" >&2',
},
)
console.log(`[STDOUT]: ${command.stdout}`)
console.log(`[STDERR]: ${command.stderr}`)
console.log(`[OUTPUT]: ${command.output}`)
// Or execute command in the background and get the logs later
const command2 = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
runAsync: true,
},
)
await new Promise((resolve) => setTimeout(resolve, 5000))
// Get the logs up to the current point in time
const logs = await sandbox.process.getSessionCommandLogs(sessionId, command2.cmdId!)
console.log(`[STDOUT]: ${logs.stdout}`)
console.log(`[STDERR]: ${logs.stderr}`)
console.log(`[OUTPUT]: ${logs.output}`)
await sandbox.delete()
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
session_id = 'exec-session-1'
sandbox.process.create_session(session_id)
# Execute a blocking command and wait for the result
command = sandbox.process.execute_session_command(
session_id,
Daytona::SessionExecuteRequest.new(
command: 'echo "Hello from stdout" && echo "Hello from stderr" >&2'
)
)
puts "[STDOUT]: #{command.stdout}"
puts "[STDERR]: #{command.stderr}"
puts "[OUTPUT]: #{command.output}"
# Or execute command in the background and get the logs later
command = sandbox.process.execute_session_command(
session_id,
Daytona::SessionExecuteRequest.new(
command: 'while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
var_async: true
)
)
sleep(5)
# Get the logs up to the current point in time
logs = sandbox.process.get_session_command_logs(session_id, command.cmd_id)
puts "[STDOUT]: #{logs.stdout}"
puts "[STDERR]: #{logs.stderr}"
puts "[OUTPUT]: #{logs.output}"
daytona.delete(sandbox)
```
```go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
sessionID := "exec-session-1"
sandbox.Process.CreateSession(ctx, sessionID)
// Execute a blocking command and wait for the result
cmd1, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID,
`echo "Hello from stdout" && echo "Hello from stderr" >&2`, false)
if stdout, ok := cmd1["stdout"].(string); ok {
fmt.Printf("[STDOUT]: %s\n", stdout)
}
if stderr, ok := cmd1["stderr"].(string); ok {
fmt.Printf("[STDERR]: %s\n", stderr)
}
// Or execute command in the background and get the logs later
cmd := `counter=1; while (( counter <= 5 )); do echo "Count: $counter"; ((counter++)); sleep 1; done`
cmdResult, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, cmd, true)
cmdID, _ := cmdResult["id"].(string)
time.Sleep(5 * time.Second)
// Get the logs up to the current point in time
logs, err := sandbox.Process.GetSessionCommandLogs(ctx, sessionID, cmdID)
if err != nil {
log.Fatalf("Failed to get logs: %v", err)
}
if logContent, ok := logs["logs"].(string); ok {
fmt.Printf("[LOGS]: %s\n", logContent)
}
sandbox.Delete(ctx)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.SessionCommandLogsResponse;
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
public class App {
public static void main(String[] args) throws InterruptedException {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
String sessionId = "exec-session-1";
sandbox.getProcess().createSession(sessionId);
SessionExecuteResponse command = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"echo 'Hello from stdout' && echo 'Hello from stderr' >&2",
false));
System.out.println("[STDOUT]: " + command.getStdout());
System.out.println("[STDERR]: " + command.getStderr());
System.out.println("[OUTPUT]: " + command.getOutput());
SessionExecuteResponse asyncCmd = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"while true; do if (( RANDOM % 2 )); then echo \"All good at $(date)\"; else echo \"Oops, an error at $(date)\" >&2; fi; sleep 1; done",
true));
Thread.sleep(5000);
SessionCommandLogsResponse logs = sandbox.getProcess().getSessionCommandLogs(sessionId, asyncCmd.getCmdId());
System.out.println("[STDOUT]: " + logs.getStdout());
System.out.println("[STDERR]: " + logs.getStderr());
System.out.println("[OUTPUT]: " + logs.getOutput());
sandbox.delete();
}
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs'
```
# Daytona MCP Server
Daytona Model Context Protocol (MCP) server enables AI agents to interact with [Daytona Sandboxes](https://www.daytona.io/docs/en/sandboxes.md) programmatically. This guide covers how to set up and use the MCP server with various AI agents.
## Install Daytona CLI
Install the Daytona CLI to manage the MCP server.
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
## Authenticate with Daytona
Authenticate with Daytona to enable MCP server access.
```bash
daytona login
```
## Initialize MCP server
Daytona provides methods to initialize the MCP server with your preferred AI agent. Supported agents include Claude, Cursor, and Windsurf.
```bash
# Initialize with Claude
daytona mcp init claude
# Initialize with Cursor
daytona mcp init cursor
# Initialize with Windsurf
daytona mcp init windsurf
```
After initialization, open your AI agent application to begin using Daytona features.
## Configure MCP server
Daytona provides methods to generate MCP configuration for integration with other AI agents.
```bash
daytona mcp config
```
This command outputs a JSON configuration that you can copy into your agent's settings:
```json
{
"mcpServers": {
"daytona-mcp": {
"command": "daytona",
"args": ["mcp", "start"],
"env": {
"HOME": "${HOME}",
"PATH": "${HOME}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"
},
"logFile": "${HOME}/Library/Logs/daytona/daytona-mcp-server.log"
}
}
}
```
:::note
For Windows users, add the following to the `env` field:
```json
"APPDATA": "${APPDATA}"
```
:::
## Start MCP server
Daytona provides methods to manually start the MCP server.
```bash
daytona mcp start
```
## Available tools
Daytona MCP server provides the following tools for interacting with Daytona Sandboxes:
- [Sandbox management](https://www.daytona.io/docs/en/sandboxes.md)
- [File system operations](https://www.daytona.io/docs/en/file-system-operations.md)
- [Git operations](https://www.daytona.io/docs/en/git-operations.md)
- [Process and code execution](https://www.daytona.io/docs/en/process-code-execution.md)
- [Computer use](https://www.daytona.io/docs/en/computer-use.md)
- [Preview](https://www.daytona.io/docs/en/preview.md)
## Troubleshooting
To troubleshoot issues with the Daytona MCP server, try the following:
- **Authentication issues**: run `daytona login` to refresh credentials
- **Connection errors**: verify MCP server configuration, check server status
- **Sandbox errors**: use `daytona list` to check sandbox status
If the issue persists, contact [support@daytona.io](mailto:support@daytona.io).
# Computer Use
Computer Use enables programmatic control of desktop environments within sandboxes. It provides mouse, keyboard, screenshot, screen recording, and display operations for automating GUI interactions and testing desktop applications.
Computer Use and [VNC](https://www.daytona.io/docs/en/vnc-access.md) 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.
Computer Use is available for **Linux**. **Windows** and **macOS** support is currently in private alpha.
:::caution[Private Alpha]
Computer Use for macOS and Windows is currently in private alpha and requires access. To request access, fill out the [Windows](https://docs.google.com/forms/d/e/1FAIpQLSfoK-77-VpfsMubw8F4f1opCxIL1AyJUgnM0ONYup5hZ0RTvQ/viewform?usp=dialog) or [macOS](https://docs.google.com/forms/d/e/1FAIpQLSc9xlGZ49OjWNkyzDPC9Ip3InMRR0ZXY3tcoD-PFQj3ck6gzQ/viewform?usp=sharing&ouid=103304973264148733944) access request form. Our team will review your request and reach out with setup instructions.
:::
- **GUI application testing**: automate interactions with native applications, click buttons, fill forms, and validate UI behavior
- **Visual testing & screenshots**: capture screenshots of applications, compare UI states, and perform visual regression testing
- **Desktop automation**: automate repetitive desktop tasks, file management through GUI, and complex workflows
## Start Computer Use
Start all computer use processes (Xvfb, xfce4, x11vnc, novnc) in the Sandbox.
```python
result = sandbox.computer_use.start()
print("Computer use processes started:", result.message)
```
```typescript
const result = await sandbox.computerUse.start();
console.log('Computer use processes started:', result.message);
```
```ruby
result = sandbox.computer_use.start
puts "Computer use processes started: #{result.message}"
```
```go
err := sandbox.ComputerUse.Start(ctx)
if err != nil {
log.Fatal(err)
}
defer sandbox.ComputerUse.Stop(ctx)
fmt.Println("Computer use processes started")
```
```java
var result = sandbox.computerUse.start();
System.out.println("Computer use processes started: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/start' \
--request POST
```
## Stop Computer Use
Stop all computer use processes in the Sandbox.
```python
result = sandbox.computer_use.stop()
print("Computer use processes stopped:", result.message)
```
```typescript
const result = await sandbox.computerUse.stop();
console.log('Computer use processes stopped:', result.message);
```
```ruby
result = sandbox.computer_use.stop
puts "Computer use processes stopped: #{result.message}"
```
```go
err := sandbox.ComputerUse.Stop(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Computer use processes stopped")
```
```java
var result = sandbox.computerUse.stop();
System.out.println("Computer use processes stopped: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/stop' \
--request POST
```
## Get status
Get the status of all computer use processes.
```python
response = sandbox.computer_use.get_status()
print("Computer use status:", response.status)
```
```typescript
const status = await sandbox.computerUse.getStatus();
console.log('Computer use status:', status.status);
```
```ruby
response = sandbox.computer_use.status
puts "Computer use status: #{response.status}"
```
```go
status, err := sandbox.ComputerUse.GetStatus(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Computer use status: %v\n", status["status"])
```
```java
var response = sandbox.computerUse.getStatus();
System.out.println("Computer use status: " + response.getStatus());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/status'
```
## Get process status
Get the status of a specific VNC process.
```python
xvfb_status = sandbox.computer_use.get_process_status("xvfb")
novnc_status = sandbox.computer_use.get_process_status("novnc")
```
```typescript
const xvfbStatus = await sandbox.computerUse.getProcessStatus('xvfb');
const noVncStatus = await sandbox.computerUse.getProcessStatus('novnc');
```
```ruby
xvfb_status = sandbox.computer_use.get_process_status("xvfb")
no_vnc_status = sandbox.computer_use.get_process_status("novnc")
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/status'
```
## Restart process
Restart a specific VNC process.
```python
result = sandbox.computer_use.restart_process("xfce4")
print("XFCE4 process restarted:", result.message)
```
```typescript
const result = await sandbox.computerUse.restartProcess('xfce4');
console.log('XFCE4 process restarted:', result.message);
```
```ruby
result = sandbox.computer_use.restart_process("xfce4")
puts "XFCE4 process restarted: #{result.message}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/restart' \
--request POST
```
## Get process logs
Get logs for a specific VNC process.
```python
logs = sandbox.computer_use.get_process_logs("novnc")
print("NoVNC logs:", logs)
```
```typescript
const logsResp = await sandbox.computerUse.getProcessLogs('novnc');
console.log('NoVNC logs:', logsResp.logs);
```
```ruby
logs = sandbox.computer_use.get_process_logs("novnc")
puts "NoVNC logs: #{logs}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/logs'
```
## Get process errors
Get error logs for a specific VNC process.
```python
errors = sandbox.computer_use.get_process_errors("x11vnc")
print("X11VNC errors:", errors)
```
```typescript
const errorsResp = await sandbox.computerUse.getProcessErrors('x11vnc');
console.log('X11VNC errors:', errorsResp.errors);
```
```ruby
errors = sandbox.computer_use.get_process_errors("x11vnc")
puts "X11VNC errors: #{errors}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/errors'
```
## Mouse operations
### Click
Click the mouse at the specified coordinates. `button` is one of `left`, `right`, or `middle` (case-insensitive; defaults to `left`); other values return an error.
```python
# Single left click
result = sandbox.computer_use.mouse.click(100, 200)
# Double click
double_click = sandbox.computer_use.mouse.click(100, 200, "left", True)
# Right click
right_click = sandbox.computer_use.mouse.click(100, 200, "right")
```
```typescript
// Single left click
const result = await sandbox.computerUse.mouse.click(100, 200);
// Double click
const doubleClick = await sandbox.computerUse.mouse.click(100, 200, 'left', true);
// Right click
const rightClick = await sandbox.computerUse.mouse.click(100, 200, 'right');
```
```ruby
# Single left click
result = sandbox.computer_use.mouse.click(x: 100, y: 200)
# Double click
double_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'left', double: true)
# Right click
right_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'right')
```
```go
// Single left click
result, err := sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, nil, nil)
if err != nil {
log.Fatal(err)
}
// Double click
doubleClick := true
result, err = sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, nil, &doubleClick)
// Right click
rightButton := "right"
result, err = sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, &rightButton, nil)
```
```java
// Single left click
sandbox.computerUse.click(100, 200);
// Double click
sandbox.computerUse.doubleClick(100, 200);
// Right click
sandbox.computerUse.click(100, 200, "right");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/click' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"button": "left",
"double": true,
"x": 100,
"y": 200
}'
```
### Move
Move the mouse cursor to the specified coordinates.
```python
result = sandbox.computer_use.mouse.move(100, 200)
print(f"Mouse moved to: {result.x}, {result.y}")
```
```typescript
const result = await sandbox.computerUse.mouse.move(100, 200);
console.log(`Mouse moved to: ${result.x}, ${result.y}`);
```
```ruby
result = sandbox.computer_use.mouse.move(x: 100, y: 200)
puts "Mouse moved to: #{result.x}, #{result.y}"
```
```go
result, err := sandbox.ComputerUse.Mouse().Move(ctx, 100, 200)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mouse moved to: %v, %v\n", result["x"], result["y"])
```
```java
var result = sandbox.computerUse.moveMouse(100, 200);
System.out.println("Mouse moved to: " + result.getX() + ", " + result.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/move' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"x": 1,
"y": 1
}'
```
### Drag
Drag the mouse from start coordinates to end coordinates.
```python
result = sandbox.computer_use.mouse.drag(50, 50, 150, 150)
print(f"Drag ended at {result.x}, {result.y}")
```
```typescript
const result = await sandbox.computerUse.mouse.drag(50, 50, 150, 150);
console.log(`Drag ended at ${result.x}, ${result.y}`);
```
```ruby
result = sandbox.computer_use.mouse.drag(start_x: 50, start_y: 50, end_x: 150, end_y: 150)
puts "Drag ended at #{result.x}, #{result.y}"
```
```go
result, err := sandbox.ComputerUse.Mouse().Drag(ctx, 50, 50, 150, 150, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Dragged to %v, %v\n", result["x"], result["y"])
```
```java
var result = sandbox.computerUse.drag(50, 50, 150, 150);
System.out.println("Drag ended at: " + result.getX() + ", " + result.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/drag' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"button": "left",
"endX": 200,
"endY": 300,
"startX": 100,
"startY": 100
}'
```
### Scroll
Scroll the mouse wheel at the specified coordinates. `direction` is `up` or `down` (other values return an error). `amount` is the number of scroll wheel ticks to send — one tick is roughly one notch of a physical mouse wheel, which moves a few lines in most apps. Defaults to 1 if omitted.
```python
# Scroll up
scroll_up = sandbox.computer_use.mouse.scroll(100, 200, "up", 3)
# Scroll down
scroll_down = sandbox.computer_use.mouse.scroll(100, 200, "down", 5)
```
```typescript
// Scroll up
const scrollUp = await sandbox.computerUse.mouse.scroll(100, 200, 'up', 3);
// Scroll down
const scrollDown = await sandbox.computerUse.mouse.scroll(100, 200, 'down', 5);
```
```ruby
# Scroll up
scroll_up = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'up', amount: 3)
# Scroll down
scroll_down = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'down', amount: 5)
```
```go
// Scroll up
amount := 3
success, err := sandbox.ComputerUse.Mouse().Scroll(ctx, 100, 200, "up", &amount)
if err != nil {
log.Fatal(err)
}
// Scroll down
amount = 5
success, err = sandbox.ComputerUse.Mouse().Scroll(ctx, 100, 200, "down", &amount)
```
```java
// Scroll up (negative vertical delta maps to "up")
sandbox.computerUse.scroll(100, 200, 0, -3);
// Scroll down
sandbox.computerUse.scroll(100, 200, 0, 5);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/scroll' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"amount": 3,
"direction": "down",
"x": 100,
"y": 200
}'
```
### Get position
Get the current mouse cursor position.
```python
position = sandbox.computer_use.mouse.get_position()
print(f"Mouse is at: {position.x}, {position.y}")
```
```typescript
const position = await sandbox.computerUse.mouse.getPosition();
console.log(`Mouse is at: ${position.x}, ${position.y}`);
```
```ruby
position = sandbox.computer_use.mouse.position
puts "Mouse is at: #{position.x}, #{position.y}"
```
```go
position, err := sandbox.ComputerUse.Mouse().GetPosition(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mouse is at: %v, %v\n", position["x"], position["y"])
```
```java
var position = sandbox.computerUse.getMousePosition();
System.out.println("Mouse is at: " + position.getX() + ", " + position.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/position'
```
## Keyboard operations
### Type
Types arbitrary text, including uppercase letters, symbols, and non-ASCII characters. Newlines (`\n`, `\r`, `\r\n`) are translated into Enter key presses; literal tab and other control characters are rejected.
```python
sandbox.computer_use.keyboard.type("Hello, World!")
# With delay between characters
sandbox.computer_use.keyboard.type("Slow typing", 100)
```
```typescript
await sandbox.computerUse.keyboard.type('Hello, World!');
// With delay between characters
await sandbox.computerUse.keyboard.type('Slow typing', 100);
```
```ruby
sandbox.computer_use.keyboard.type(text: "Hello, World!")
# With delay between characters
sandbox.computer_use.keyboard.type(text: "Slow typing", delay: 100)
```
```go
err := sandbox.ComputerUse.Keyboard().Type(ctx, "Hello, World!", nil)
if err != nil {
log.Fatal(err)
}
// With delay between characters
delay := 100
err = sandbox.ComputerUse.Keyboard().Type(ctx, "Slow typing", &delay)
```
```java
sandbox.computerUse.typeText("Hello, World!");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/type' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"delay": 1,
"text": ""
}'
```
### Press
Press a key with optional modifiers.
```python
# Press Enter
sandbox.computer_use.keyboard.press("enter")
# Press Ctrl+C
sandbox.computer_use.keyboard.press("c", ["ctrl"])
# Press Ctrl+Shift+T
sandbox.computer_use.keyboard.press("t", ["ctrl", "shift"])
```
```typescript
// Press Enter
await sandbox.computerUse.keyboard.press('enter');
// Press Ctrl+C
await sandbox.computerUse.keyboard.press('c', ['ctrl']);
// Press Ctrl+Shift+T
await sandbox.computerUse.keyboard.press('t', ['ctrl', 'shift']);
```
```ruby
# Press Enter
sandbox.computer_use.keyboard.press(key: "enter")
# Press Ctrl+C
sandbox.computer_use.keyboard.press(key: "c", modifiers: ["ctrl"])
# Press Ctrl+Shift+T
sandbox.computer_use.keyboard.press(key: "t", modifiers: ["ctrl", "shift"])
```
```go
// Press Enter
err := sandbox.ComputerUse.Keyboard().Press(ctx, "enter", nil)
if err != nil {
log.Fatal(err)
}
// Press Ctrl+C
err = sandbox.ComputerUse.Keyboard().Press(ctx, "c", []string{"ctrl"})
// Press Ctrl+Shift+T
err = sandbox.ComputerUse.Keyboard().Press(ctx, "t", []string{"ctrl", "shift"})
```
```java
// Press Enter
sandbox.computerUse.pressKey("enter");
// Press Ctrl+C
sandbox.computerUse.pressHotkey("ctrl", "c");
// Press Ctrl+Shift+T
sandbox.computerUse.pressHotkey("ctrl", "shift", "t");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/key' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"key": "enter",
"modifiers": []
}'
```
### Hotkey
Press a hotkey combination.
```python
# Copy
sandbox.computer_use.keyboard.hotkey("ctrl+c")
# Paste
sandbox.computer_use.keyboard.hotkey("ctrl+v")
# Alt+Tab
sandbox.computer_use.keyboard.hotkey("alt+tab")
```
```typescript
// Copy
await sandbox.computerUse.keyboard.hotkey('ctrl+c');
// Paste
await sandbox.computerUse.keyboard.hotkey('ctrl+v');
// Alt+Tab
await sandbox.computerUse.keyboard.hotkey('alt+tab');
```
```ruby
# Copy
sandbox.computer_use.keyboard.hotkey(keys: "ctrl+c")
# Paste
sandbox.computer_use.keyboard.hotkey(keys: "ctrl+v")
# Alt+Tab
sandbox.computer_use.keyboard.hotkey(keys: "alt+tab")
```
```go
// Copy
err := sandbox.ComputerUse.Keyboard().Hotkey(ctx, "ctrl+c")
if err != nil {
log.Fatal(err)
}
// Paste
err = sandbox.ComputerUse.Keyboard().Hotkey(ctx, "ctrl+v")
// Alt+Tab
err = sandbox.ComputerUse.Keyboard().Hotkey(ctx, "alt+tab")
```
```java
// Copy
sandbox.computerUse.pressHotkey("ctrl", "c");
// Paste
sandbox.computerUse.pressHotkey("ctrl", "v");
// Alt+Tab
sandbox.computerUse.pressHotkey("alt", "tab");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/hotkey' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"keys": "ctrl+c"
}'
```
### Supported keys
`keyboard.press()` and `keyboard.hotkey()` are case-insensitive for named keys. The following are supported:
| Category | Keys |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| Modifiers | `ctrl`, `alt`, `shift`, `cmd` |
| Editing | `enter`, `escape`, `tab`, `backspace`, `delete`, `space` |
| Navigation | `home`, `end`, `pageup`, `pagedown`, `insert`, arrow keys (`up`, `down`, `left`, `right`) |
| Function keys | `f1` through `f24` |
| Numpad | `num0`–`num9`, `num_plus`, `num_minus`, `num_asterisk`, `num_slash`, `num_decimal`, `num_enter`, `num_equal`, `num_lock` |
| Letters and digits | `a`–`z` (case-insensitive), `0`–`9` |
| Punctuation | `` ` `` `-` `=` `[` `]` `\` `;` `'` `,` `.` `/` |
| Other | `capslock`, `menu` |
Common aliases like `Return` → `enter`, `control` → `ctrl`, `command` / `meta` / `win` → `cmd`, and `option` → `alt` are normalized automatically. Unsupported or malformed inputs return an error, sometimes with a suggested alternative.
## Accessibility operations
Use Linux accessibility operations to inspect the AT-SPI tree and interact with UI elements by node ID. Start Computer Use before calling accessibility methods.
:::note[App accessibility support]
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
Read an accessibility tree for the focused app, a specific process, or all apps.
```python
# Focused app
focused_tree = sandbox.computer_use.accessibility.get_tree(scope="focused", max_depth=2)
# Specific process
process_tree = sandbox.computer_use.accessibility.get_tree(
scope="pid",
pid=1234,
max_depth=2,
)
# All apps
desktop_tree = sandbox.computer_use.accessibility.get_tree(scope="all", max_depth=2)
```
```typescript
// Focused app
const focusedTree = await sandbox.computerUse.accessibility.getTree({
scope: 'focused',
maxDepth: 2,
});
// Specific process
const processTree = await sandbox.computerUse.accessibility.getTree({
scope: 'pid',
pid: 1234,
maxDepth: 2,
});
// All apps
const desktopTree = await sandbox.computerUse.accessibility.getTree({
scope: 'all',
maxDepth: 2,
});
```
```ruby
# Focused app
focused_tree = sandbox.computer_use.accessibility.get_tree(scope: "focused", max_depth: 2)
# Specific process
process_tree = sandbox.computer_use.accessibility.get_tree(
scope: "pid",
pid: 1234,
max_depth: 2
)
# All apps
desktop_tree = sandbox.computer_use.accessibility.get_tree(scope: "all", max_depth: 2)
```
```go
maxDepth := 2
// Focused app
focusedScope := "focused"
focusedTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &focusedScope,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
// Specific process
processScope := "pid"
pid := 1234
processTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &processScope,
PID: &pid,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
// All apps
allScope := "all"
desktopTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &allScope,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
```
```java
// Focused app
var focusedTree = sandbox.computerUse.getAccessibilityTree("focused", null, 2);
// Specific process
var processTree = sandbox.computerUse.getAccessibilityTree("pid", 1234, 2);
// All apps
var desktopTree = sandbox.computerUse.getAccessibilityTree("all", null, 2);
```
```bash
# Focused app
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=focused&maxDepth=2'
# Specific process
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=pid&pid=1234&maxDepth=2'
# All apps
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=all&maxDepth=2'
```
### Find nodes
Search the accessibility tree by role, accessible name, state, and scope.
```python
# Find buttons by accessible name
buttons = sandbox.computer_use.accessibility.find_nodes(
scope="focused",
role="button",
name="Submit",
name_match="substring",
limit=10,
)
# Find text entries in a process
entries = sandbox.computer_use.accessibility.find_nodes(
scope="pid",
pid=1234,
role="entry",
states=["enabled", "focusable"],
limit=10,
)
# Find visible nodes across all apps
visible_nodes = sandbox.computer_use.accessibility.find_nodes(
scope="all",
states=["visible"],
limit=20,
)
```
```typescript
// Find buttons by accessible name
const buttons = await sandbox.computerUse.accessibility.findNodes({
scope: 'focused',
role: 'button',
name: 'Submit',
nameMatch: 'substring',
limit: 10,
});
// Find text entries in a process
const entries = await sandbox.computerUse.accessibility.findNodes({
scope: 'pid',
pid: 1234,
role: 'entry',
states: ['enabled', 'focusable'],
limit: 10,
});
// Find visible nodes across all apps
const visibleNodes = await sandbox.computerUse.accessibility.findNodes({
scope: 'all',
states: ['visible'],
limit: 20,
});
```
```ruby
# Find buttons by accessible name
buttons = sandbox.computer_use.accessibility.find_nodes(
scope: "focused",
role: "button",
name: "Submit",
name_match: "substring",
limit: 10
)
# Find text entries in a process
entries = sandbox.computer_use.accessibility.find_nodes(
scope: "pid",
pid: 1234,
role: "entry",
states: ["enabled", "focusable"],
limit: 10
)
# Find visible nodes across all apps
visible_nodes = sandbox.computer_use.accessibility.find_nodes(
scope: "all",
states: ["visible"],
limit: 20
)
```
```go
limit := 10
// Find buttons by accessible name
focusedScope := "focused"
buttonRole := "button"
submitName := "Submit"
substringMatch := "substring"
buttons, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &focusedScope,
Role: &buttonRole,
Name: &submitName,
NameMatch: &substringMatch,
Limit: &limit,
})
if err != nil {
log.Fatal(err)
}
// Find text entries in a process
processScope := "pid"
pid := 1234
entryRole := "entry"
entries, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &processScope,
PID: &pid,
Role: &entryRole,
States: []string{"enabled", "focusable"},
Limit: &limit,
})
if err != nil {
log.Fatal(err)
}
// Find visible nodes across all apps
allScope := "all"
visibleLimit := 20
visibleNodes, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &allScope,
States: []string{"visible"},
Limit: &visibleLimit,
})
if err != nil {
log.Fatal(err)
}
```
```java
// Find buttons by accessible name
var buttons = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("focused")
.role("button")
.name("Submit")
.nameMatch("substring")
.limit(10)
);
// Find text entries in a process
var entries = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("pid")
.pid(1234)
.role("entry")
.states(java.util.List.of("enabled", "focusable"))
.limit(10)
);
// Find visible nodes across all apps
var visibleNodes = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("all")
.states(java.util.List.of("visible"))
.limit(20)
);
```
```bash
# Find buttons by accessible name
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "focused",
"role": "button",
"name": "Submit",
"nameMatch": "substring",
"limit": 10
}'
# Find text entries in a process
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "pid",
"pid": 1234,
"role": "entry",
"states": ["enabled", "focusable"],
"limit": 10
}'
# Find visible nodes across all apps
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "all",
"states": ["visible"],
"limit": 20
}'
```
### Focus node
Move keyboard focus to a node returned by `get_tree` or `find_nodes`.
```python
sandbox.computer_use.accessibility.focus_node("node-id")
```
```typescript
await sandbox.computerUse.accessibility.focusNode('node-id');
```
```ruby
sandbox.computer_use.accessibility.focus_node(id: "node-id")
```
```go
if err := sandbox.ComputerUse.Accessibility().FocusNode(ctx, "node-id"); err != nil {
log.Fatal(err)
}
```
```java
sandbox.computerUse.focusAccessibilityNode("node-id");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/focus' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id"}'
```
### Invoke node
Run a node action, such as pressing a button.
```python
# Invoke the primary action
sandbox.computer_use.accessibility.invoke_node("node-id")
# Invoke a named action
sandbox.computer_use.accessibility.invoke_node("node-id", action="click")
```
```typescript
// Invoke the primary action
await sandbox.computerUse.accessibility.invokeNode('node-id');
// Invoke a named action
await sandbox.computerUse.accessibility.invokeNode('node-id', 'click');
```
```ruby
# Invoke the primary action
sandbox.computer_use.accessibility.invoke_node(id: "node-id")
# Invoke a named action
sandbox.computer_use.accessibility.invoke_node(id: "node-id", action: "click")
```
```go
// Invoke the primary action
if err := sandbox.ComputerUse.Accessibility().InvokeNode(ctx, "node-id", nil); err != nil {
log.Fatal(err)
}
// Invoke a named action
action := "click"
if err := sandbox.ComputerUse.Accessibility().InvokeNode(ctx, "node-id", &action); err != nil {
log.Fatal(err)
}
```
```java
// Invoke the primary action
sandbox.computerUse.invokeAccessibilityNode("node-id");
// Invoke a named action
sandbox.computerUse.invokeAccessibilityNode("node-id", "click");
```
```bash
# Invoke the primary action
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/invoke' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id"}'
# Invoke a named action
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/invoke' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id","action":"click"}'
```
### Set node value
Write text or value content to nodes that support value changes.
```python
sandbox.computer_use.accessibility.set_node_value("node-id", "hello")
```
```typescript
await sandbox.computerUse.accessibility.setNodeValue('node-id', 'hello');
```
```ruby
sandbox.computer_use.accessibility.set_node_value(id: "node-id", value: "hello")
```
```go
if err := sandbox.ComputerUse.Accessibility().SetNodeValue(ctx, "node-id", "hello"); err != nil {
log.Fatal(err)
}
```
```java
sandbox.computerUse.setAccessibilityNodeValue("node-id", "hello");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/value' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id","value":"hello"}'
```
## Screenshot operations
### Take full screen
Take a screenshot of the entire screen.
```python
screenshot = sandbox.computer_use.screenshot.take_full_screen()
print(f"Screenshot size: {screenshot.width}x{screenshot.height}")
# With cursor visible
with_cursor = sandbox.computer_use.screenshot.take_full_screen(True)
```
```typescript
const screenshot = await sandbox.computerUse.screenshot.takeFullScreen();
console.log(`Screenshot size: ${screenshot.width}x${screenshot.height}`);
// With cursor visible
const withCursor = await sandbox.computerUse.screenshot.takeFullScreen(true);
```
```ruby
screenshot = sandbox.computer_use.screenshot.take_full_screen
puts "Screenshot size: #{screenshot.width}x#{screenshot.height}"
# With cursor visible
with_cursor = sandbox.computer_use.screenshot.take_full_screen(show_cursor: true)
```
```go
screenshot, err := sandbox.ComputerUse.Screenshot().TakeFullScreen(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Screenshot captured, size: %d bytes\n", *screenshot.SizeBytes)
// With cursor visible
showCursor := true
withCursor, err := sandbox.ComputerUse.Screenshot().TakeFullScreen(ctx, &showCursor)
```
```java
var screenshot = sandbox.computerUse.takeScreenshot();
Integer sizeBytes = screenshot.getSizeBytes();
System.out.println("Screenshot payload size: " + (sizeBytes != null ? sizeBytes + " bytes" : "n/a"));
// With cursor visible
var withCursor = sandbox.computerUse.takeScreenshot(true);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot'
```
### Take region
Take a screenshot of a specific region.
```python
from daytona import ScreenshotRegion
region = ScreenshotRegion(x=100, y=100, width=300, height=200)
screenshot = sandbox.computer_use.screenshot.take_region(region)
print(f"Captured region: {screenshot.region.width}x{screenshot.region.height}")
```
```typescript
const region = { x: 100, y: 100, width: 300, height: 200 };
const screenshot = await sandbox.computerUse.screenshot.takeRegion(region);
console.log(`Captured region: ${screenshot.region.width}x${screenshot.region.height}`);
```
```ruby
region = Daytona::ComputerUse::ScreenshotRegion.new(x: 100, y: 100, width: 300, height: 200)
screenshot = sandbox.computer_use.screenshot.take_region(region: region)
puts "Captured region: #{screenshot.region.width}x#{screenshot.region.height}"
```
```go
region := types.ScreenshotRegion{X: 100, Y: 100, Width: 300, Height: 200}
screenshot, err := sandbox.ComputerUse.Screenshot().TakeRegion(ctx, region, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Captured region: %dx%d\n", screenshot.Width, screenshot.Height)
```
```java
var screenshot = sandbox.computerUse.takeRegionScreenshot(100, 100, 300, 200);
Integer sizeBytes = screenshot.getSizeBytes();
System.out.println("Captured region, payload size: " + (sizeBytes != null ? sizeBytes + " bytes" : "n/a"));
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/region?x=1&y=1&width=1&height=1'
```
### Take compressed
Take a compressed screenshot of the entire screen.
```python
from daytona import ScreenshotOptions
# Default compression
screenshot = sandbox.computer_use.screenshot.take_compressed()
# High quality JPEG
jpeg = sandbox.computer_use.screenshot.take_compressed(
ScreenshotOptions(format="jpeg", quality=95, show_cursor=True)
)
# Scaled down PNG
scaled = sandbox.computer_use.screenshot.take_compressed(
ScreenshotOptions(format="png", scale=0.5)
)
```
```typescript
// Default compression
const screenshot = await sandbox.computerUse.screenshot.takeCompressed();
// High quality JPEG
const jpeg = await sandbox.computerUse.screenshot.takeCompressed({
format: 'jpeg',
quality: 95,
showCursor: true
});
// Scaled down PNG
const scaled = await sandbox.computerUse.screenshot.takeCompressed({
format: 'png',
scale: 0.5
});
```
```ruby
# Default compression
screenshot = sandbox.computer_use.screenshot.take_compressed
# High quality JPEG
jpeg = sandbox.computer_use.screenshot.take_compressed(
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "jpeg", quality: 95, show_cursor: true)
)
# Scaled down PNG
scaled = sandbox.computer_use.screenshot.take_compressed(
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "png", scale: 0.5)
)
```
```java
// Compressed full screen (format, quality 1-100, scale factor)
var screenshot = sandbox.computerUse.takeCompressedScreenshot("png", 80, 1.0);
// High quality JPEG at full scale
var jpeg = sandbox.computerUse.takeCompressedScreenshot("jpeg", 95, 1.0);
// Scaled down PNG
var scaled = sandbox.computerUse.takeCompressedScreenshot("png", 80, 0.5);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/compressed'
```
### Take compressed region
Take a compressed screenshot of a specific region.
```python
from daytona import ScreenshotRegion, ScreenshotOptions
region = ScreenshotRegion(x=0, y=0, width=800, height=600)
screenshot = sandbox.computer_use.screenshot.take_compressed_region(
region,
ScreenshotOptions(format="webp", quality=80, show_cursor=True)
)
print(f"Compressed size: {screenshot.size_bytes} bytes")
```
```typescript
const region = { x: 0, y: 0, width: 800, height: 600 };
const screenshot = await sandbox.computerUse.screenshot.takeCompressedRegion(region, {
format: 'webp',
quality: 80,
showCursor: true
});
console.log(`Compressed size: ${screenshot.size_bytes} bytes`);
```
```ruby
region = Daytona::ComputerUse::ScreenshotRegion.new(x: 0, y: 0, width: 800, height: 600)
screenshot = sandbox.computer_use.screenshot.take_compressed_region(
region: region,
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "webp", quality: 80, show_cursor: true)
)
puts "Compressed size: #{screenshot.size_bytes} bytes"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/region/compressed?x=1&y=1&width=1&height=1'
```
## Screen Recording
Computer Use supports screen recording capabilities, allowing you to capture desktop sessions for debugging, documentation, or automation workflows.
### Configure Recording Directory
By default, recordings are saved to `~/.daytona/recordings`. You can specify a custom directory by passing the `DAYTONA_RECORDINGS_DIR` environment variable when creating a sandbox:
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytonaio/sandbox:0.6.0",
name="my-sandbox",
env_vars={"DAYTONA_RECORDINGS_DIR": "/home/daytona/my-recordings"}
)
)
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create({
snapshot: 'daytonaio/sandbox:0.6.0',
name: 'my-sandbox',
envVars: { DAYTONA_RECORDINGS_DIR: '/home/daytona/my-recordings' }
});
```
```ruby
require 'daytona'
daytona = Daytona::Client.new
sandbox = daytona.create(
snapshot: 'daytonaio/sandbox:0.6.0',
name: 'my-sandbox',
env_vars: { DAYTONA_RECORDINGS_DIR: '/home/daytona/my-recordings' }
)
```
```go
import (
"github.com/daytonaio/daytona/pkg/client"
"github.com/daytonaio/daytona/pkg/types"
)
daytona := client.New()
envVars := map[string]string{
"DAYTONA_RECORDINGS_DIR": "/home/daytona/my-recordings",
}
sandbox, err := daytona.Create(ctx, &types.CreateSandboxParams{
Snapshot: "daytonaio/sandbox:0.6.0",
Name: "my-sandbox",
EnvVars: envVars,
})
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import java.util.Map;
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytonaio/sandbox:0.6.0");
params.setName("my-sandbox");
params.setEnvVars(Map.of("DAYTONA_RECORDINGS_DIR", "/home/daytona/my-recordings"));
Sandbox sandbox = daytona.create(params);
}
```
### Start Recording
Start a new screen recording session with an optional name identifier:
```python
# Start recording with a custom name
recording = sandbox.computer_use.recording.start("test-1")
print(f"Recording started: {recording.id}")
print(f"File path: {recording.file_path}")
```
```typescript
// Start recording with a custom name
const recording = await sandbox.computerUse.recording.start('test-1');
console.log(`Recording started: ${recording.id}`);
console.log(`File path: ${recording.file_path}`);
```
```ruby
# Start recording with a custom label
recording = sandbox.computer_use.recording.start(label: 'test-1')
puts "Recording started: #{recording.id}"
puts "File path: #{recording.file_path}"
```
```go
// Start recording with a custom name
name := "test-1"
recording, err := sandbox.ComputerUse.Recording().Start(ctx, &name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording started: %s\n", *recording.Id)
fmt.Printf("File path: %s\n", *recording.FilePath)
```
```java
// Start recording with a custom label
var recording = sandbox.computerUse.startRecording("test-1");
System.out.println("Recording started: " + recording.getId());
System.out.println("File path: " + recording.getFilePath());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/start' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"name": "test-1"
}'
```
### Stop Recording
Stop an active recording session by providing the recording ID:
```python
# Stop the recording
stopped_recording = sandbox.computer_use.recording.stop(recording.id)
print(f"Recording stopped: {stopped_recording.duration_seconds} seconds")
print(f"Saved to: {stopped_recording.file_path}")
```
```typescript
// Stop the recording
const stoppedRecording = await sandbox.computerUse.recording.stop(recording.id);
console.log(`Recording stopped: ${stoppedRecording.duration_seconds} seconds`);
console.log(`Saved to: ${stoppedRecording.file_path}`);
```
```ruby
# Stop the recording
stopped_recording = sandbox.computer_use.recording.stop(id: recording.id)
puts "Recording stopped: #{stopped_recording.duration_seconds} seconds"
puts "Saved to: #{stopped_recording.file_path}"
```
```go
// Stop the recording
stoppedRecording, err := sandbox.ComputerUse.Recording().Stop(ctx, *recording.Id)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording stopped: %f seconds\n", *stoppedRecording.DurationSeconds)
fmt.Printf("Saved to: %s\n", *stoppedRecording.FilePath)
```
```java
// Stop the recording
var stoppedRecording = sandbox.computerUse.stopRecording(recording.getId());
System.out.println("Recording stopped: " + stoppedRecording.getDurationSeconds() + " seconds");
System.out.println("Saved to: " + stoppedRecording.getFilePath());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/stop' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"id": "recording-id"
}'
```
### List Recordings
Get a list of all recordings in the sandbox:
```python
recordings_list = sandbox.computer_use.recording.list()
print(f"Total recordings: {len(recordings_list.recordings)}")
for rec in recordings_list.recordings:
print(f"- {rec.name}: {rec.duration_seconds}s ({rec.file_size_bytes} bytes)")
```
```typescript
const recordingsList = await sandbox.computerUse.recording.list();
console.log(`Total recordings: ${recordingsList.recordings.length}`);
recordingsList.recordings.forEach(rec => {
console.log(`- ${rec.name}: ${rec.duration_seconds}s (${rec.file_size_bytes} bytes)`);
});
```
```ruby
recordings_list = sandbox.computer_use.recording.list
puts "Total recordings: #{recordings_list.recordings.length}"
recordings_list.recordings.each do |rec|
puts "- #{rec.name}: #{rec.duration_seconds}s (#{rec.file_size_bytes} bytes)"
end
```
```go
recordingsList, err := sandbox.ComputerUse.Recording().List(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total recordings: %d\n", len(recordingsList.Recordings))
for _, rec := range recordingsList.Recordings {
fmt.Printf("- %s: %.2fs (%d bytes)\n", *rec.Name, *rec.DurationSeconds, *rec.FileSizeBytes)
}
```
```java
var recordingsList = sandbox.computerUse.listRecordings();
System.out.println("Total recordings: " + recordingsList.getRecordings().size());
for (var rec : recordingsList.getRecordings()) {
System.out.println(
"- " + rec.getFileName() + ": " + rec.getDurationSeconds() + "s (" + rec.getSizeBytes() + " bytes)"
);
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings'
```
### Get Recording
Get details about a specific recording:
```python
recording_detail = sandbox.computer_use.recording.get("recording-id")
print(f"Recording: {recording_detail.name}")
print(f"Status: {recording_detail.status}")
print(f"Duration: {recording_detail.duration_seconds}s")
```
```typescript
const recordingDetail = await sandbox.computerUse.recording.get('recording-id');
console.log(`Recording: ${recordingDetail.name}`);
console.log(`Status: ${recordingDetail.status}`);
console.log(`Duration: ${recordingDetail.duration_seconds}s`);
```
```ruby
recording_detail = sandbox.computer_use.recording.get(id: 'recording-id')
puts "Recording: #{recording_detail.name}"
puts "Status: #{recording_detail.status}"
puts "Duration: #{recording_detail.duration_seconds}s"
```
```go
recordingDetail, err := sandbox.ComputerUse.Recording().Get(ctx, "recording-id")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording: %s\n", *recordingDetail.Name)
fmt.Printf("Status: %s\n", *recordingDetail.Status)
fmt.Printf("Duration: %.2fs\n", *recordingDetail.DurationSeconds)
```
```java
var recordingDetail = sandbox.computerUse.getRecording("recording-id");
System.out.println("Recording: " + recordingDetail.getFileName());
System.out.println("Status: " + recordingDetail.getStatus());
System.out.println("Duration: " + recordingDetail.getDurationSeconds() + "s");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}'
```
### Delete Recording
Delete a recording by ID:
```python
sandbox.computer_use.recording.delete("recording-id")
print("Recording deleted successfully")
```
```typescript
await sandbox.computerUse.recording.delete('recording-id');
console.log('Recording deleted successfully');
```
```ruby
sandbox.computer_use.recording.delete(id: 'recording-id')
puts 'Recording deleted successfully'
```
```go
err := sandbox.ComputerUse.Recording().Delete(ctx, "recording-id")
if err != nil {
log.Fatal(err)
}
fmt.Println("Recording deleted successfully")
```
```java
sandbox.computerUse.deleteRecording("recording-id");
System.out.println("Recording deleted successfully");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}' \
--request DELETE
```
### Download Recording
Download a recording file from the sandbox to your local machine. The file is streamed efficiently without loading the entire content into memory, making it suitable for large recordings.
```python
# Download recording to local file
sandbox.computer_use.recording.download(recording.id, "local_recording.mp4")
print("Recording downloaded successfully")
# Or with custom path
import os
download_path = os.path.join("recordings", f"recording_{recording.id}.mp4")
sandbox.computer_use.recording.download(recording.id, download_path)
```
```typescript
// Download recording to local file
await sandbox.computerUse.recording.download(recording.id, 'local_recording.mp4');
console.log('Recording downloaded successfully');
// Or with custom path
const downloadPath = `recordings/recording_${recording.id}.mp4`;
await sandbox.computerUse.recording.download(recording.id, downloadPath);
```
```ruby
# Download recording to local file
sandbox.computer_use.recording.download(id: recording.id, local_path: 'local_recording.mp4')
puts 'Recording downloaded successfully'
# Or with custom path
download_path = "recordings/recording_#{recording.id}.mp4"
sandbox.computer_use.recording.download(id: recording.id, local_path: download_path)
```
```go
// Download recording to local file
err := sandbox.ComputerUse.Recording().Download(ctx, recording.GetId(), "local_recording.mp4")
if err != nil {
log.Fatal(err)
}
fmt.Println("Recording downloaded successfully")
// Or with custom path
downloadPath := fmt.Sprintf("recordings/recording_%s.mp4", recording.GetId())
err = sandbox.ComputerUse.Recording().Download(ctx, recording.GetId(), downloadPath)
```
```java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
// Download returns a temp file from the API client; copy it to a stable path
var tempFile = sandbox.computerUse.downloadRecording(recording.getId());
Files.copy(tempFile.toPath(), Path.of("local_recording.mp4"), StandardCopyOption.REPLACE_EXISTING);
System.out.println("Recording saved to local_recording.mp4");
var downloadPath = Path.of("recordings", "recording_" + recording.getId() + ".mp4");
Files.createDirectories(downloadPath.getParent());
Files.copy(tempFile.toPath(), downloadPath, StandardCopyOption.REPLACE_EXISTING);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}/download' \
--output local_recording.mp4
```
:::tip[Streaming Downloads]
All SDK implementations stream the recording file directly to disk without loading the entire content into memory. This allows you to download large recordings (hundreds of MB or even GB) efficiently without running out of memory.
- **Python**: Streams in 64KB chunks using `httpx`
- **TypeScript**: Uses Node.js `pipeline()` with backpressure handling
- **Ruby**: Uses Typhoeus streaming with `on_body` callbacks
- **Go**: Uses `io.Copy()` with 32KB internal buffer
- **Java**: The OpenAPI client streams the response body into a temporary file via OkHttp
:::
### Recording Dashboard
Every sandbox includes a built-in recording dashboard for managing screen recordings through a web interface. The dashboard allows you to view, download, and delete recordings without writing code.
To access the recording dashboard:
1. Navigate to your sandboxes in the Daytona Dashboard
2. Click the action menu (three dots) for your sandbox
3. Select **Screen Recordings** from the dropdown menu
The recording dashboard provides:
- List of all recordings with metadata (name, duration, file size, creation time)
- Playback controls for reviewing recordings
- Download functionality to save recordings locally
- Delete options for managing storage
:::tip
The recording dashboard runs on a private port and is automatically secured. No additional authentication is required once you access it through the Daytona Dashboard.
:::
## Display operations
### Get info
Get information about the displays.
```python
info = sandbox.computer_use.display.get_info()
print(f"Primary display: {info.primary_display.width}x{info.primary_display.height}")
print(f"Total displays: {info.total_displays}")
for i, display in enumerate(info.displays):
print(f"Display {i}: {display.width}x{display.height} at {display.x},{display.y}")
```
```typescript
const info = await sandbox.computerUse.display.getInfo();
console.log(`Primary display: ${info.primary_display.width}x${info.primary_display.height}`);
console.log(`Total displays: ${info.total_displays}`);
info.displays.forEach((display, index) => {
console.log(`Display ${index}: ${display.width}x${display.height} at ${display.x},${display.y}`);
});
```
```ruby
info = sandbox.computer_use.display.info
puts "Primary display: #{info.primary_display.width}x#{info.primary_display.height}"
puts "Total displays: #{info.total_displays}"
info.displays.each_with_index do |display, i|
puts "Display #{i}: #{display.width}x#{display.height} at #{display.x},#{display.y}"
end
```
```go
info, err := sandbox.ComputerUse.Display().GetInfo(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Displays: %v\n", info["displays"])
```
```java
var info = sandbox.computerUse.getDisplayInfo();
if (info.getDisplays() != null) {
for (var display : info.getDisplays()) {
System.out.println(
"Display " + display.getId() + ": " + display.getWidth() + "x" + display.getHeight()
+ " at " + display.getX() + "," + display.getY()
);
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/display/info'
```
### Get windows
Get the list of open windows.
```python
windows = sandbox.computer_use.display.get_windows()
print(f"Found {windows.count} open windows:")
for window in windows.windows:
print(f"- {window.title} (ID: {window.id})")
```
```typescript
const windows = await sandbox.computerUse.display.getWindows();
console.log(`Found ${windows.count} open windows:`);
windows.windows.forEach(window => {
console.log(`- ${window.title} (ID: ${window.id})`);
});
```
```ruby
windows = sandbox.computer_use.display.windows
puts "Found #{windows.count} open windows:"
windows.windows.each do |window|
puts "- #{window.title} (ID: #{window.id})"
end
```
```go
result, err := sandbox.ComputerUse.Display().GetWindows(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Open windows: %v\n", result["windows"])
```
```java
var windows = sandbox.computerUse.getWindows();
var list = windows.getWindows();
if (list != null) {
System.out.println("Found " + list.size() + " open windows:");
for (var window : list) {
System.out.println("- " + window.getTitle() + " (ID: " + window.getId() + ")");
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/display/windows'
```
# Web Terminal
Daytona provides a browser-based web terminal for interacting with your sandboxes. The web terminal allows you to run commands, view files, and debug directly from your browser without installing any local tools.
- **Remote command execution**: run shell commands directly in your sandbox
- **File management**: navigate the file system, view and edit files
- **Debugging**: inspect logs, monitor processes, and troubleshoot issues
- **Package management**: install dependencies and configure your environment
## Access from Dashboard
Access the web terminal directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Navigate to [Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the running sandbox you want to access
3. Click the terminal icon **`>_`**
This opens the web terminal in a new browser tab, providing a full terminal session connected to your sandbox. The web terminal is available only for sandboxes in the `STARTED` state. If your sandbox is stopped, start it before attempting to access the terminal.
## Access via CLI
When you create a sandbox using the Daytona CLI, the web terminal URL is displayed automatically in the output.
```shell
daytona create
```
The CLI output includes the terminal URL:
```text
Sandbox '' created successfully
Connect via SSH: daytona ssh
Open the Web Terminal: https://22222-.proxy.daytona.work
```
## Access via URL
The web terminal runs on port `22222` inside each sandbox. You can obtain the terminal URL programmatically using [Preview URLs](https://www.daytona.io/docs/en/preview.md).
Pass port `22222` to the preview URL method:
```python
terminal_info = sandbox.get_preview_link(22222)
print(f"Web Terminal URL: {terminal_info.url}")
```
```typescript
const terminalInfo = await sandbox.getPreviewLink(22222);
console.log(`Web Terminal URL: ${terminalInfo.url}`);
```
```ruby
terminal_info = sandbox.preview_url(22222)
puts "Web Terminal URL: #{terminal_info.url}"
```
```go
url, err := sandbox.GetPreviewLink(ctx, 22222)
```
```bash
daytona preview-url --port 22222
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/22222/preview-url' \
--header 'Authorization: Bearer '
```
## Security
Terminal access is restricted to authenticated members of your [Organization](https://www.daytona.io/docs/en/organizations.md). Even when a sandbox has its `public` parameter set to `true`, the web terminal remains accessible only to organization members.
:::warning
The web terminal provides full shell access to your sandbox. Treat terminal URLs with the same care as SSH credentials. Do not share terminal URLs with untrusted parties.
:::
## Related
- [SSH Access](https://www.daytona.io/docs/en/ssh-access.md): connect to your sandbox from a local terminal or IDE
- [Pseudo Terminal (PTY)](https://www.daytona.io/docs/en/pty.md): programmatic terminal sessions for automated workflows
- [Process and Code Execution](https://www.daytona.io/docs/en/process-code-execution.md): execute commands and run code in sandboxes
- [Preview](https://www.daytona.io/docs/en/preview.md): generate preview URLs for accessing sandbox services
# SSH Access
Daytona provides SSH access to your sandboxes using token-based authentication. This allows you to connect from local terminals, IDEs, and development tools without installing additional software.
## Access from Dashboard
Create an SSH access token directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Navigate to [Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the sandbox you want to create an SSH access token for
3. Click the sandbox options menu (**⋮**)
4. Select **Create SSH Access**
5. Set the expiration time (defaults to 60 minutes)
6. Click **Create**
Daytona generates a token and displays it in the modal. Copy the token and use it to connect to your sandbox.
## Access via CLI
Daytona provides a CLI command to create an SSH access token for a sandbox:
```shell
daytona create
```
When you create a sandbox, Daytona displays the SSH command automatically in the output:
```text
Sandbox '' created successfully
Connect via SSH: daytona ssh
Open the Web Terminal: https://22222-.proxy.daytona.work
```
To SSH into an existing sandbox, use the following command:
```bash
daytona ssh --expires 60
```
## Access via token
You can create SSH access tokens programmatically. The token can then be used to connect manually:
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.get("sandbox-abc123")
# Create SSH access token
ssh_access = sandbox.create_ssh_access(expires_in_minutes=60)
print(f"SSH Token: {ssh_access.token}")
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.get('sandbox-abc123')
// Create SSH access token
const sshAccess = await sandbox.createSshAccess(60)
console.log(`SSH Token: ${sshAccess.token}`)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.get('sandbox-abc123')
# Create SSH access token
ssh_access = sandbox.create_ssh_access(expires_in_minutes: 60)
puts "SSH Token: #{ssh_access.token}"
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access?expiresInMinutes=60' \
--request POST \
--header 'Authorization: Bearer '
```
To connect to your sandbox, use the following command:
```bash
ssh @ssh.app.daytona.io
```
## Connect with VS Code
You can connect VS Code directly to your sandbox using the Remote SSH extension.
1. Install the [Remote Explorer extension ↗](https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-explorer)
2. Add a new SSH connection
3. When prompted for the SSH connection URL, paste the SSH command from above
For more information, see the [VS Code Remote SSH documentation ↗](https://code.visualstudio.com/docs/remote/ssh).
## Connect with JetBrains IDEs
JetBrains Gateway provides remote development support for connecting to your sandbox.
1. Download [JetBrains Gateway ↗](https://www.jetbrains.com/remote-development/gateway/)
2. Add a new connection
3. When prompted for the SSH connection URL, paste the SSH command from above
4. Select the IDE to install in your sandbox
## Token management
### Expiration
SSH access tokens expire automatically after 60 minutes. You can specify a custom expiration time when creating the token using the `expires_in_minutes` parameter.
### Revoke token
Revoke SSH access tokens before expiry:
```python
# Revoke specific SSH access token for the sandbox
sandbox.revoke_ssh_access(token="specific-token")
```
```typescript
// Revoke specific SSH access token for the sandbox
await sandbox.revokeSshAccess('specific-token')
```
```ruby
# Revoke specific SSH access token for the sandbox
sandbox.revoke_ssh_access(token: 'specific-token')
```
```bash
# Revoke specific SSH access token
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access?token=specific-token' \
--request DELETE \
--header 'Authorization: Bearer '
# Revoke all SSH access for the sandbox
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access' \
--request DELETE \
--header 'Authorization: Bearer '
```
## Related
- [Web Terminal](https://www.daytona.io/docs/en/web-terminal.md): browser-based terminal access to sandboxes
- [Preview](https://www.daytona.io/docs/en/preview.md): generate preview URLs for accessing sandbox services
# VNC Access
VNC (Virtual Network Computing) access provides a graphical desktop environment for your Daytona Sandbox directly in the browser. This allows you to interact with GUI applications, desktop tools, and visual interfaces running inside your sandbox.
VNC and [Computer Use](https://www.daytona.io/docs/en/computer-use.md) 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 mouse, keyboard, and screenshot operations. Through VNC, you can observe AI agents performing automated tasks via Computer Use in real-time.
- **GUI application development**: build and test desktop applications with visual interfaces
- **Browser testing**: run and debug web applications in a full browser environment
- **Visual debugging**: inspect graphical output and UI behavior in real-time
- **Desktop tool access**: use graphical IDEs, design tools, or other desktop software
- **Agent observation**: watch AI agents perform automated tasks through Computer Use
:::note[Sandbox image requirement]
VNC and Computer Use require a sandbox with the default image. Sandboxes created with custom images do not include VNC support unless you install the [required packages](#required-packages).
:::
## Access VNC from Dashboard
Access the VNC desktop environment directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Navigate to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the sandbox you want to access via VNC
3. Click the options menu (**⋮**) next to the sandbox
4. Select **VNC** from the dropdown menu
This opens a VNC viewer in your browser with a **Connect** button.
5. Click **Connect** to establish the VNC session
Once connected, a full desktop environment loads in your browser, providing mouse and keyboard control over the sandbox's graphical interface.
:::note
VNC sessions remain active as long as the sandbox is running. If the sandbox auto-stops due to inactivity, you need to start the sandbox again before reconnecting via VNC.
:::
## Programmatic VNC management
Daytona provides methods to [start](#start-vnc), [stop](#stop-vnc), and [monitor](#get-vnc-status) VNC sessions and processes programmatically using the [Computer Use](https://www.daytona.io/docs/en/computer-use.md) references as part of automated workflows.
### Start VNC
Start all VNC processes (Xvfb, xfce4, x11vnc, novnc) in the sandbox to enable desktop access.
```python
result = sandbox.computer_use.start()
print("VNC processes started:", result.message)
```
```typescript
const result = await sandbox.computerUse.start();
console.log('VNC processes started:', result.message);
```
```ruby
result = sandbox.computer_use.start
puts "VNC processes started: #{result.message}"
```
```go
err := sandbox.ComputerUse.Start(ctx)
if err != nil {
log.Fatal(err)
}
defer sandbox.ComputerUse.Stop(ctx)
fmt.Println("VNC processes started")
```
```java
var result = sandbox.computerUse.start();
System.out.println("VNC processes started: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/start' \
--request POST
```
### Stop VNC
Stop all VNC processes in the sandbox.
```python
result = sandbox.computer_use.stop()
print("VNC processes stopped:", result.message)
```
```typescript
const result = await sandbox.computerUse.stop();
console.log('VNC processes stopped:', result.message);
```
```ruby
result = sandbox.computer_use.stop
puts "VNC processes stopped: #{result.message}"
```
```go
err := sandbox.ComputerUse.Stop(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("VNC processes stopped")
```
```java
var result = sandbox.computerUse.stop();
System.out.println("VNC processes stopped: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/stop' \
--request POST
```
### Get VNC status
Check the status of VNC processes to verify they are running.
```python
response = sandbox.computer_use.get_status()
print("VNC status:", response.status)
```
```typescript
const status = await sandbox.computerUse.getStatus();
console.log('VNC status:', status.status);
```
```ruby
response = sandbox.computer_use.status
puts "VNC status: #{response.status}"
```
```go
status, err := sandbox.ComputerUse.GetStatus(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("VNC status: %v\n", status["status"])
```
```java
var response = sandbox.computerUse.getStatus();
System.out.println("VNC status: " + response.getStatus());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/status'
```
For additional process management operations including restarting individual processes and viewing logs, see the [Computer Use](https://www.daytona.io/docs/en/computer-use.md) reference.
## Automating desktop interactions
Once VNC is running, you can automate desktop interactions using Computer Use. This enables AI agents to programmatically control the mouse, keyboard, and capture screenshots within the VNC session.
**Available operations:**
- **Mouse**: click, move, drag, scroll, and get cursor position
- **Keyboard**: type text, press keys, and execute hotkey combinations
- **Screenshot**: capture full screen, regions, or compressed images
- **Display**: get display information and list open windows
For complete documentation on automating desktop interactions, see [Computer Use](https://www.daytona.io/docs/en/computer-use.md).
> **Example**: Automated browser interaction
```python
# Start VNC processes
sandbox.computer_use.start()
# Click to open browser
sandbox.computer_use.mouse.click(50, 50)
# Type a URL
sandbox.computer_use.keyboard.type("https://www.daytona.io/docs/")
sandbox.computer_use.keyboard.press("enter")
# Take a screenshot
screenshot = sandbox.computer_use.screenshot.take_full_screen()
```
```typescript
// Start VNC processes
await sandbox.computerUse.start();
// Click to open browser
await sandbox.computerUse.mouse.click(50, 50);
// Type a URL
await sandbox.computerUse.keyboard.type('https://www.daytona.io/docs/');
await sandbox.computerUse.keyboard.press('enter');
// Take a screenshot
const screenshot = await sandbox.computerUse.screenshot.takeFullScreen();
```
```java
// Start VNC processes
sandbox.computerUse.start();
// Click to open browser
sandbox.computerUse.click(50, 50);
// Type a URL
sandbox.computerUse.typeText("https://www.daytona.io/docs/");
sandbox.computerUse.pressKey("enter");
// Take a screenshot
var screenshot = sandbox.computerUse.takeScreenshot();
```
## Required packages
The default sandbox image includes all packages required for VNC and Computer Use. If you are using a custom image, you need to install the following packages.
### VNC and desktop environment
| Package | Description |
| -------------------- | ------------------------------------------ |
| **`xvfb`** | X Virtual Framebuffer for headless display |
| **`xfce4`** | Desktop environment |
| **`xfce4-terminal`** | Terminal emulator |
| **`x11vnc`** | VNC server |
| **`novnc`** | Web-based VNC client |
| **`dbus-x11`** | D-Bus session support |
### X11 libraries
| Library | Description |
| ----------------- | ------------------------------------------- |
| **`libx11-6`** | X11 client library |
| **`libxrandr2`** | X11 RandR extension (display configuration) |
| **`libxext6`** | X11 extensions library |
| **`libxrender1`** | X11 rendering extension |
| **`libxfixes3`** | X11 fixes extension |
| **`libxss1`** | X11 screen saver extension |
| **`libxtst6`** | X11 testing extension (input simulation) |
| **`libxi6`** | X11 input extension |
# VPN Connections
VPN connections are a way to connect your Daytona sandboxes to private networks. By establishing a VPN connection, your sandbox can access network resources using private IP addresses and can be accessed by other devices on the same VPN network.
This integration enables communication between your development environment and existing infrastructure, allowing you to test applications against services within the private network, access shared development resources, and collaborate with team members.
Daytona supports the following VPN network providers:
- [Tailscale](#tailscale)
- [OpenVPN](#openvpn)
- [Netbird](#netbird)
:::note
For connecting to a VPN network, you need to [create or access your Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md), **have access to your VPN network provider credentials**, and be on [**Tier 3** or higher](https://www.daytona.io/docs/en/limits.md#resources).
:::
## Tailscale
Daytona provides multiple ways to connect to a Daytona sandbox with a Tailscale network:
- [Connect with browser login](#tailscale-browser-login)
- [Connect with auth key](#tailscale-auth-key)
- [Connect with web terminal](#tailscale-web-terminal)
When you connect a Daytona sandbox to a Tailscale network, the sandbox becomes part of your private Tailscale network, allowing you to access resources that are available within the network and enabling other devices on the network to access the sandbox.
This integration makes your sandbox appear as a device within your Tailscale network, with its own Tailscale IP address and access to other devices and services on the network.
### Tailscale browser login
The browser login method initiates an interactive authentication flow where Tailscale generates a unique login URL that you visit in your web browser to authenticate the Dayona sandbox.
The process involves installing Tailscale, starting the daemon, initiating the login process, and then polling for the authentication status until the connection is established.
The following snippet demonstrates connecting to a Tailscale network using a browser login.
```python
from daytona import Daytona, DaytonaConfig
import time
import re
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_tailscale_vpn_interactive():
"""
Connect a Daytona sandbox to a Tailscale network using the Python SDK.
Uses interactive login via browser URL (no auth key required).
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install Tailscale
print("\nInstalling Tailscale (this may take a few minutes)...")
response = sandbox.process.exec(
"curl -fsSL https://tailscale.com/install.sh | sh",
timeout=300
)
if response.exit_code != 0:
print(f"Error installing Tailscale: {response.result}")
return sandbox
print("Tailscale installed successfully.")
# Step 2: Start tailscaled daemon in background
print("\nStarting tailscaled daemon...")
sandbox.process.exec("nohup sudo tailscaled > /dev/null 2>&1 &", timeout=10)
# Wait for daemon to initialize
time.sleep(3)
# Step 3: Run tailscale up in background and capture output to a file
print("\nInitiating Tailscale login...")
sandbox.process.exec(
"sudo tailscale up > /tmp/tailscale-login.txt 2>&1 &",
timeout=10
)
# Wait for the login URL to be written to the file
time.sleep(3)
# Read the login URL from the output file
response = sandbox.process.exec("cat /tmp/tailscale-login.txt", timeout=10)
output = response.result
url_match = re.search(r'https://login\.tailscale\.com/a/[^\s]+', output)
if url_match:
login_url = url_match.group(0)
print(f"\n{'='*60}")
print("To authenticate, visit this URL in your browser:")
print(f"\n {login_url}")
print(f"\n{'='*60}")
print("\nWaiting for authentication...")
# Poll for connection status
max_wait = 300
poll_interval = 5
waited = 0
while waited < max_wait:
time.sleep(poll_interval)
waited += poll_interval
status_response = sandbox.process.exec("tailscale status 2>&1", timeout=30)
status_output = status_response.result
# Check if connected
if status_response.exit_code == 0 and "logged out" not in status_output.lower():
# Verify IP is assigned
ip_response = sandbox.process.exec("tailscale ip -4 2>&1", timeout=10)
if ip_response.exit_code == 0 and ip_response.result.strip():
print(f"\nConnected to Tailscale network!")
print(f"Tailscale IP: {ip_response.result.strip()}")
break
print(f" Still waiting... ({waited}s)")
else:
print("\nTimeout waiting for authentication. Please try again.")
return sandbox
else:
# Already connected or different output
print(f"Output from tailscale up:\n{output}")
# Check if already connected
status_response = sandbox.process.exec("tailscale status", timeout=30)
if status_response.exit_code == 0:
print("\nTailscale status:")
print(status_response.result)
# Final status check
print("\nFinal Tailscale status:")
response = sandbox.process.exec("tailscale status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_tailscale_vpn_interactive()
```
```typescript
import { Daytona } from '@daytona/sdk'
// Configuration
const DAYTONA_API_KEY = 'YOUR_API_KEY' // Replace with your API key
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
})
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function setupTailscaleVpnInteractive(): Promise {
/**
* Connect a Daytona sandbox to a Tailscale network using the TypeScript SDK.
* Uses interactive login via browser URL (no auth key required).
*/
// Create the sandbox
console.log('Creating sandbox...')
const sandbox = await daytona.create()
console.log(`Sandbox created: ${sandbox.id}`)
// Step 1: Install Tailscale
console.log('\nInstalling Tailscale (this may take a few minutes)...')
let response = await sandbox.process.executeCommand(
'curl -fsSL https://tailscale.com/install.sh | sh',
undefined, // cwd
undefined, // env
300 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error installing Tailscale: ${response.result}`)
return
}
console.log('Tailscale installed successfully.')
// Step 2: Start tailscaled daemon in background
console.log('\nStarting tailscaled daemon...')
await sandbox.process.executeCommand(
'nohup sudo tailscaled > /dev/null 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for daemon to initialize
await sleep(3000)
// Step 3: Run tailscale up in background and capture output to a file
console.log('\nInitiating Tailscale login...')
await sandbox.process.executeCommand(
'sudo tailscale up > /tmp/tailscale-login.txt 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for the login URL to be written to the file
await sleep(3000)
// Read the login URL from the output file
response = await sandbox.process.executeCommand(
'cat /tmp/tailscale-login.txt',
undefined, // cwd
undefined, // env
10 // timeout
)
const output = response.result || ''
const urlMatch = output.match(/https:\/\/login\.tailscale\.com\/a\/[^\s]+/)
if (urlMatch) {
const loginUrl = urlMatch[0]
console.log('\n' + '='.repeat(60))
console.log('To authenticate, visit this URL in your browser:')
console.log(`\n ${loginUrl}`)
console.log('\n' + '='.repeat(60))
console.log('\nWaiting for authentication...')
// Poll for connection status
const maxWait = 300 // 5 minutes max wait
const pollInterval = 5
let waited = 0
while (waited < maxWait) {
await sleep(pollInterval * 1000)
waited += pollInterval
const statusResponse = await sandbox.process.executeCommand(
'tailscale status 2>&1',
undefined, // cwd
undefined, // env
30 // timeout
)
const statusOutput = statusResponse.result || ''
// Check if we're connected (status shows our machine without login prompt)
if (
statusResponse.exitCode === 0 &&
!statusOutput.toLowerCase().includes('logged out')
) {
// Verify we have an IP assigned
const ipResponse = await sandbox.process.executeCommand(
'tailscale ip -4 2>&1',
undefined, // cwd
undefined, // env
10 // timeout
)
if (ipResponse.exitCode === 0 && ipResponse.result?.trim()) {
console.log('\nConnected to Tailscale network!')
console.log(`Tailscale IP: ${ipResponse.result.trim()}`)
break
}
}
console.log(` Still waiting... (${waited}s)`)
}
if (waited >= maxWait) {
console.log('\nTimeout waiting for authentication. Please try again.')
return
}
} else {
// Maybe already connected or different output
console.log(`Output from tailscale up:\n${output}`)
// Check if already connected
const statusResponse = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
if (statusResponse.exitCode === 0) {
console.log('\nTailscale status:')
console.log(statusResponse.result)
}
}
// Final status check
console.log('\nFinal Tailscale status:')
response = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
console.log(response.result)
}
// Run the main function
setupTailscaleVpnInteractive().catch(console.error)
```
Once the connection is established and authentication is complete, the sandbox will maintain its connection as long as the service is running.
### Tailscale auth key
Using an auth key provides a non-interactive way to connect your Daytona sandbox to Tailscale, making it suitable for automated scripts, CI/CD pipelines, or any scenario where manual browser interaction is not available.
1. Access your [Tailscale admin console ↗](https://login.tailscale.com/admin/machines)
2. Click **Add device** and select **Linux server**
3. Apply the configuration and click **Generate install script**
This will generate a script that you can use to install Tailscale and connect to the Tailscale network.
```bash
curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --auth-key=tskey-auth-
```
Copy the auth key from the generated script and use it to connect your Daytona sandbox to Tailscale:
```python
from daytona import Daytona, DaytonaConfig
import time
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
TAILSCALE_AUTH_KEY = "YOUR_AUTH_KEY" # Replace with your auth key
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_tailscale_vpn(auth_key: str):
"""
Connect a Daytona sandbox to a Tailscale network using the Python SDK.
Uses auth-key for non-interactive authentication.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install Tailscale
print("\nInstalling Tailscale (this may take a few minutes)...")
response = sandbox.process.exec(
"curl -fsSL https://tailscale.com/install.sh | sh",
timeout=300
)
if response.exit_code != 0:
print(f"Error installing Tailscale: {response.result}")
return sandbox
print("Tailscale installed successfully.")
# Step 2: Start tailscaled daemon manually (systemd doesn't auto-start in sandboxes)
print("\nStarting tailscaled daemon...")
sandbox.process.exec("nohup sudo tailscaled > /dev/null 2>&1 &", timeout=10)
# Wait for daemon to initialize
time.sleep(3)
# Step 3: Connect with auth key
print("\nConnecting to Tailscale network...")
response = sandbox.process.exec(
f"sudo tailscale up --auth-key={auth_key}",
timeout=60
)
if response.exit_code != 0:
print(f"Error connecting: {response.result}")
return sandbox
print("Connected to Tailscale network.")
# Verify connection status
print("\nChecking Tailscale status...")
response = sandbox.process.exec("tailscale status", timeout=30)
print(f"Status:\n{response.result}")
return sandbox
if __name__ == "__main__":
sandbox = setup_tailscale_vpn(TAILSCALE_AUTH_KEY)
```
```typescript
import { Daytona } from '@daytona/sdk'
// Configuration
const DAYTONA_API_KEY = 'YOUR_API_KEY' // Replace with your API key
const TAILSCALE_AUTH_KEY = 'YOUR_AUTH_KEY' // Replace with your auth key
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
})
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function setupTailscaleVpn(authKey: string): Promise {
/**
* Connect a Daytona sandbox to a Tailscale network using the TypeScript SDK.
* Uses auth-key for non-interactive authentication.
*/
// Create the sandbox
console.log('Creating sandbox...')
const sandbox = await daytona.create()
console.log(`Sandbox created: ${sandbox.id}`)
// Step 1: Install Tailscale
console.log('\nInstalling Tailscale (this may take a few minutes)...')
let response = await sandbox.process.executeCommand(
'curl -fsSL https://tailscale.com/install.sh | sh',
undefined, // cwd
undefined, // env
300 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error installing Tailscale: ${response.result}`)
return
}
console.log('Tailscale installed successfully.')
// Step 2: Start tailscaled daemon manually (systemd doesn't auto-start in sandboxes)
console.log('\nStarting tailscaled daemon...')
await sandbox.process.executeCommand(
'nohup sudo tailscaled > /dev/null 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for daemon to initialize
await sleep(3000)
// Step 3: Connect with auth key
console.log('\nConnecting to Tailscale network...')
response = await sandbox.process.executeCommand(
`sudo tailscale up --auth-key=${authKey}`,
undefined, // cwd
undefined, // env
60 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error connecting: ${response.result}`)
return
}
console.log('Connected to Tailscale network.')
// Verify connection status
console.log('\nChecking Tailscale status...')
response = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
console.log(`Status:\n${response.result}`)
}
// Run the main function
setupTailscaleVpn(TAILSCALE_AUTH_KEY).catch(console.error)
```
Once the connection is established and authentication is complete, the sandbox will maintain its connection as long as the service is running.
### Tailscale web terminal
For working directly in the terminal or for more control over the Tailscale connection process, you can set up Tailscale manually through the Daytona [web terminal](https://www.daytona.io/docs/en/web-terminal.md) or [SSH](https://www.daytona.io/docs/en/ssh-access.md).
This approach provides visibility into each step of the installation and connection process, and allows you to customize the setup if needed. The process involves installing Tailscale, starting the daemon in a persistent session using tmux, and then authenticating through the interactive login flow.
1. Navigate to your sandbox [web terminal](https://www.daytona.io/docs/en/web-terminal.md) in [Daytona Dashboard ↗](https://app.daytona.io/), or [access it via SSH](https://www.daytona.io/docs/en/ssh-access.md)
2. Install Tailscale using the official installation script:
```bash
curl -fsSL https://tailscale.com/install.sh | sh
```
This begins the Tailscale installation process and initializes the Tailscale CLI inside the sandbox. Daytona requires the Tailscale daemon to be running in the background to connect the sandbox to the Tailscale network.
The recommended approach is to run it in a detached tmux (or similar) session to ensure the daemon is running in the background:
3. Install tmux
```bash
sudo apt install tmux
```
4. Start the Tailscale daemon in a detached tmux session
```bash
tmux new -d -s tailscale 'sudo tailscaled'
```
5. Connect and authenticate your sandbox with the Tailscale network
```bash
sudo tailscale up
```
6. Visit the authentication URL in the web browser and follow the instructions to authenticate
```txt
To authenticate, visit:
https://login.tailscale.com/a/
```
Once authenticated, you will see the following confirmation message:
```txt
Your device is logged in to the tailnet.
```
You've now successfully connected your Daytona sandbox to your Tailscale network. The sandbox should appear in your [Tailscale dashboard](https://login.tailscale.com/admin/machines).
## OpenVPN
Daytona provides multiple ways to connect to a Daytona sandbox with an OpenVPN network:
- [OpenVPN client configuration](#openvpn-client-configuration)
- [Connect with web terminal](#openvpn-web-terminal)
OpenVPN uses a client-server model where your Daytona sandbox acts as a client connecting to an OpenVPN server. This approach is suitable for connecting to existing corporate VPNs, accessing resources behind firewalls, or integrating with infrastructure that already uses OpenVPN.
:::note
Connecting a Daytona sandbox to OpenVPN network requires a [client configuration file](#openvpn-client-configuration-file).
:::
### OpenVPN client configuration file
Client configuration file contains the connection parameters, certificates, and keys required to establish a secure connection to your OpenVPN server. This file is typically provided by your network administrator or generated from your OpenVPN server setup.
The configuration file should be named `client.ovpn` or similar, and it must contain all the required connection settings, including server address, port, protocol, encryption settings, and authentication credentials. To create this file, you can use a text editor such as nano or vim, or upload it to your sandbox if you have it prepared elsewhere.
The following snippet is an example of a client configuration file. Replace the placeholders with the actual values provided by your OpenVPN server.
```bash
client
proto udp
explicit-exit-notify
remote
dev tun
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verify-x509-name name
auth SHA256
auth-nocache
cipher AES-128-GCM
ignore-unknown-option data-ciphers
data-ciphers AES-128-GCM
ncp-ciphers AES-128-GCM
tls-client
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256
tls-ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256
ignore-unknown-option block-outside-dns
setenv opt block-outside-dns # Prevent Windows 10 DNS leak
verb 3
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
-----BEGIN OpenVPN tls-crypt-v2 client key-----
-----END OpenVPN tls-crypt-v2 client key-----
```
### OpenVPN client configuration
The following snippets demonstrate connecting to an OpenVPN network using a client configuration file.
```python
from daytona import Daytona, DaytonaConfig
import time
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
# OpenVPN client configuration (paste your .ovpn config here)
OPENVPN_CONFIG = """
""".strip()
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_openvpn(ovpn_config: str):
"""
Connect a Daytona sandbox to an OpenVPN network using the Python SDK.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install OpenVPN
print("\nInstalling OpenVPN...")
response = sandbox.process.exec(
"sudo apt update && sudo apt install -y openvpn",
timeout=120
)
if response.exit_code != 0:
print(f"Error installing OpenVPN: {response.result}")
return sandbox
print("OpenVPN installed successfully.")
# Step 2: Write the OpenVPN config file
print("\nWriting OpenVPN configuration...")
sandbox.fs.upload_file(ovpn_config.encode(), "/home/daytona/client.ovpn")
print("Configuration written to /home/daytona/client.ovpn")
# Step 3: Start OpenVPN in background
print("\nStarting OpenVPN tunnel...")
sandbox.process.exec(
"nohup sudo openvpn /home/daytona/client.ovpn > /tmp/openvpn.log 2>&1 &",
timeout=10
)
# Wait for connection to establish
print("Waiting for VPN connection to establish...")
time.sleep(10)
# Step 4: Verify connection
print("\nVerifying OpenVPN connection...")
# Check if tun interface exists
response = sandbox.process.exec("ip addr show tun0", timeout=10)
if response.exit_code == 0:
print("VPN tunnel interface (tun0) is up:")
print(response.result)
else:
print("Warning: tun0 interface not found. Checking OpenVPN logs...")
log_response = sandbox.process.exec("cat /tmp/openvpn.log", timeout=10)
print(f"OpenVPN log:\n{log_response.result}")
return sandbox
# Get public IP through VPN
print("\nChecking public IP (should be VPN server IP)...")
response = sandbox.process.exec("curl -s ifconfig.me", timeout=30)
if response.exit_code == 0:
print(f"Public IP: {response.result}")
else:
print(f"Could not determine public IP: {response.result}")
print("\nOpenVPN connection established successfully.")
return sandbox
if __name__ == "__main__":
sandbox = setup_openvpn(OPENVPN_CONFIG)
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY"; // Replace with your API key
// OpenVPN client configuration (paste your .ovpn config here)
const OPENVPN_CONFIG = `
`.trim();
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function setupOpenvpn(ovpnConfig: string): Promise {
/**
* Connect a Daytona sandbox to an OpenVPN network using the TypeScript SDK.
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Step 1: Install OpenVPN
console.log("\nInstalling OpenVPN...");
let response = await sandbox.process.executeCommand(
"sudo apt update && sudo apt install -y openvpn",
undefined, // cwd
undefined, // env
120 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing OpenVPN: ${response.result}`);
return;
}
console.log("OpenVPN installed successfully.");
// Step 2: Write the OpenVPN config file
console.log("\nWriting OpenVPN configuration...");
// Use heredoc to write the config file
await sandbox.process.executeCommand(
`cat << 'OVPNEOF' > /home/daytona/client.ovpn
${ovpnConfig}
OVPNEOF`,
undefined,
undefined,
30
);
console.log("Configuration written to /home/daytona/client.ovpn");
// Step 3: Start OpenVPN in background
console.log("\nStarting OpenVPN tunnel...");
await sandbox.process.executeCommand(
"nohup sudo openvpn /home/daytona/client.ovpn > /tmp/openvpn.log 2>&1 &",
undefined, // cwd
undefined, // env
10 // timeout
);
// Wait for connection to establish
console.log("Waiting for VPN connection to establish...");
await sleep(10000);
// Step 4: Verify connection
console.log("\nVerifying OpenVPN connection...");
// Check if tun interface exists
response = await sandbox.process.executeCommand(
"ip addr show tun0",
undefined, // cwd
undefined, // env
10 // timeout
);
if (response.exitCode === 0) {
console.log("VPN tunnel interface (tun0) is up:");
console.log(response.result);
} else {
console.log("Warning: tun0 interface not found. Checking OpenVPN logs...");
const logResponse = await sandbox.process.executeCommand(
"cat /tmp/openvpn.log",
undefined, // cwd
undefined, // env
10 // timeout
);
console.log(`OpenVPN log:\n${logResponse.result}`);
return;
}
// Get public IP through VPN
console.log("\nChecking public IP (should be VPN server IP)...");
response = await sandbox.process.executeCommand(
"curl -s ifconfig.me",
undefined, // cwd
undefined, // env
30 // timeout
);
if (response.exitCode === 0) {
console.log(`Public IP: ${response.result}`);
} else {
console.log(`Could not determine public IP: ${response.result}`);
}
console.log("\nOpenVPN connection established successfully.");
}
// Run the main function
setupOpenvpn(OPENVPN_CONFIG).catch(console.error);
```
### OpenVPN web terminal
Daytona provides a [web terminal](https://www.daytona.io/docs/en/web-terminal.md) for interacting with your sandboxes, allowing you to install OpenVPN and connect to your OpenVPN network.
1. Navigate to your sandbox [web terminal](https://www.daytona.io/docs/en/web-terminal.md) in [Daytona Dashboard ↗](https://app.daytona.io/) by clicking on the Terminal icon `>_`, or [access it via SSH](https://www.daytona.io/docs/en/ssh-access.md)
2. Install OpenVPN and tmux
```bash
sudo apt update && sudo apt install -y openvpn tmux
```
3. Create the OpenVPN [client configuration file](#openvpn-client-configuration-file) for your Daytona sandbox
```bash
sudo nano client.ovpn
```
4. Save the file by pressing `Ctrl+O`, then `Enter`, and exit by pressing `Ctrl+X`
Daytona requires the OpenVPN tunnel to be running in the background to connect the sandbox to the OpenVPN network. The recommended approach is to run it in a detached tmux (or similar) session:
5. Start OpenVPN tunnel in a background tmux session
```bash
tmux new -d -s openvpn 'sudo openvpn client.ovpn'
```
This starts the OpenVPN tunnel in a background tmux session that persists even if you disconnect from the sandbox.
6. Verify the OpenVPN connection by running the following command
```bash
curl ifconfig.me
```
This will return the IP address of the sandbox connected to the OpenVPN network.
You've now successfully connected your Daytona sandbox to your OpenVPN network.
## Netbird
Daytona provides multiple ways to connect to a Daytona sandbox with a Netbird network:
- [Connect with setup key](#netbird-setup-key)
- [Connect with browser login](#netbird-browser-login)
When you connect a Daytona sandbox to a Netbird network, the sandbox becomes part of your private Netbird network, allowing you to access resources that are available within the network and enabling other devices on the network to access the sandbox.
This integration makes your sandbox appear as a device within your Netbird network, with its own Netbird IP address and access to other devices and services on the network.
### Netbird setup key
Using a Netbird setup key provides a non-interactive way to connect your Daytona sandbox to Netbird, making it suitable for automated scripts, CI/CD pipelines, or any scenario where manual browser interaction is not available.
1. Access your [Netbird dashboard ↗](https://app.netbird.io/)
2. Click **Setup Keys**
3. Create a new setup key or use an existing one
The following snippets demonstrate connecting to a Netbird network using a setup key.
```python
from daytona import Daytona, DaytonaConfig
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY"
NETBIRD_SETUP_KEY = "YOUR_SETUP_KEY"
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_netbird():
"""
Connect a Daytona sandbox to a NetBird network using the Python SDK.
Uses the official NetBird install script and a Setup Key for non-interactive auth.
See: https://docs.netbird.io/manage/peers/access-infrastructure/setup-keys-add-servers-to-network
"""
if NETBIRD_SETUP_KEY == "YOUR_SETUP_KEY":
print("Error: please set NETBIRD_SETUP_KEY to a valid setup key.")
return None
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
print("\nInstalling NetBird via the official install script...")
install_cmd = (
"sudo rm -f /etc/apt/sources.list.d/yarn.list && "
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh"
)
response = sandbox.process.exec(install_cmd, timeout=300)
if response.exit_code != 0:
print(f"Error installing NetBird: {response.result}")
return sandbox
print("NetBird installed successfully.")
# Connect to the NetBird network using the setup key
print("\nConnecting to NetBird network with setup key...")
up_cmd = f"sudo netbird up --setup-key {NETBIRD_SETUP_KEY}"
response = sandbox.process.exec(up_cmd, timeout=120)
if response.exit_code != 0:
print(f"Error running 'netbird up': {response.result}")
return sandbox
print(response.result)
# Final status check
print("\nFinal NetBird status:")
response = sandbox.process.exec("netbird status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_netbird()
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY";
const NETBIRD_SETUP_KEY = "YOUR_SETUP_KEY";
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
async function setupNetbird(): Promise {
/**
* Connect a Daytona sandbox to a NetBird network using the TypeScript SDK.
* Uses the official NetBird install script and a Setup Key for non-interactive auth.
* See: https://docs.netbird.io/manage/peers/access-infrastructure/setup-keys-add-servers-to-network
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Install NetBird via the official install script
console.log("\nInstalling NetBird via the official install script...");
const installCmd = [
"sudo rm -f /etc/apt/sources.list.d/yarn.list",
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh",
].join(" && ");
let response = await sandbox.process.executeCommand(
installCmd,
undefined, // cwd
undefined, // env
300 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing NetBird: ${response.result}`);
return;
}
console.log("NetBird installed successfully.");
// Connect to the NetBird network using the setup key
console.log("\nConnecting to NetBird network with setup key...");
const upCmd = `sudo netbird up --setup-key ${NETBIRD_SETUP_KEY}`;
response = await sandbox.process.executeCommand(
upCmd,
undefined, // cwd
undefined, // env
120 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error running 'netbird up': ${response.result}`);
return;
}
console.log(response.result);
// Final status check
console.log("\nFinal NetBird status:");
response = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
console.log(response.result);
}
// Run the main function
setupNetbird().catch(console.error);
```
### Netbird browser login
The browser login method initiates an interactive authentication flow where Netbird generates a unique login URL that you visit in your web browser to authenticate the Daytona sandbox.
The following snippets demonstrate connecting to a Netbird network using a browser login.
```python
from daytona import Daytona, DaytonaConfig
import time
import re
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY"
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_netbird():
"""
Connect a Daytona sandbox to a NetBird network using the Python SDK.
Uses the official NetBird install script and interactive browser login.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Install NetBird via the official install script
print("\nInstalling NetBird via the official install script...")
install_cmd = (
"sudo rm -f /etc/apt/sources.list.d/yarn.list && "
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh"
)
response = sandbox.process.exec(install_cmd, timeout=300)
if response.exit_code != 0:
print(f"Error installing NetBird: {response.result}")
return sandbox
print("NetBird installed successfully.")
# Run netbird up in background and capture output to a file
print("\nInitiating NetBird login...")
sandbox.process.exec(
"netbird up > /tmp/netbird-login.txt 2>&1 &",
timeout=10
)
# Wait for the login URL to be written to the file
time.sleep(5)
# Read the login URL from the output file
response = sandbox.process.exec("cat /tmp/netbird-login.txt", timeout=10)
output = response.result
url_match = re.search(r'https?://[^\s]+', output)
if url_match:
login_url = url_match.group(0)
print(f"\n{'='*60}")
print("To authenticate, visit this URL in your browser:")
print(f"\n {login_url}")
print(f"\n{'='*60}")
print("\nWaiting for authentication...")
# Poll for connection status
max_wait = 300 # 5 minutes max wait
poll_interval = 5
waited = 0
while waited < max_wait:
time.sleep(poll_interval)
waited += poll_interval
status_response = sandbox.process.exec("netbird status 2>&1", timeout=30)
status_output = status_response.result
# Check if we're connected
if status_response.exit_code == 0 and "connected" in status_output.lower():
print(f"\nConnected to NetBird network!")
print(f"NetBird status:\n{status_output}")
break
print(f" Still waiting... ({waited}s)")
else:
print("\nTimeout waiting for authentication. Please try again.")
return sandbox
else:
# Maybe already connected or different output
print(f"Output from netbird up:\n{output}")
# Check if already connected
status_response = sandbox.process.exec("netbird status", timeout=30)
if status_response.exit_code == 0:
print("\nNetBird status:")
print(status_response.result)
# Final status check
print("\nFinal NetBird status:")
response = sandbox.process.exec("netbird status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_netbird()
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY";
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function setupNetbirdVpnInteractive(): Promise {
/**
* Connect a Daytona sandbox to a NetBird network using the TypeScript SDK.
* Uses the official NetBird install script and interactive browser login.
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Remove the broken Yarn list so apt-get update inside the installer succeeds,
// then run the official NetBird install script. The script adds the NetBird
// APT repo, imports the signing key, installs the package, and registers the
// systemd service.
console.log("\nInstalling NetBird via the official install script...");
const installCmd = [
"sudo rm -f /etc/apt/sources.list.d/yarn.list",
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh",
].join(" && ");
let response = await sandbox.process.executeCommand(
installCmd,
undefined, // cwd
undefined, // env
300 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing NetBird: ${response.result}`);
return;
}
console.log("NetBird installed successfully.");
// Run netbird up in background and capture output to a file
console.log("\nInitiating NetBird login...");
await sandbox.process.executeCommand(
"netbird up > /tmp/netbird-login.txt 2>&1 &",
undefined, // cwd
undefined, // env
10 // timeout
);
// Wait for the login URL to be written to the file
await sleep(5000);
// Read the login URL from the output file
response = await sandbox.process.executeCommand(
"cat /tmp/netbird-login.txt",
undefined, // cwd
undefined, // env
10 // timeout
);
const output = response.result || "";
const urlMatch = output.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
const loginUrl = urlMatch[0];
console.log("\n" + "=".repeat(60));
console.log("To authenticate, visit this URL in your browser:");
console.log(`\n ${loginUrl}`);
console.log("\n" + "=".repeat(60));
console.log("\nWaiting for authentication...");
// Poll for connection status
const maxWait = 300; // 5 minutes max wait
const pollInterval = 5;
let waited = 0;
while (waited < maxWait) {
await sleep(pollInterval * 1000);
waited += pollInterval;
const statusResponse = await sandbox.process.executeCommand(
"netbird status 2>&1",
undefined, // cwd
undefined, // env
30 // timeout
);
const statusOutput = statusResponse.result || "";
// Check if we're connected
if (statusResponse.exitCode === 0 && statusOutput.toLowerCase().includes("connected")) {
console.log("\nConnected to NetBird network!");
console.log(`NetBird status:\n${statusOutput}`);
break;
}
console.log(` Still waiting... (${waited}s)`);
}
if (waited >= maxWait) {
console.log("\nTimeout waiting for authentication. Please try again.");
return;
}
} else {
// Maybe already connected or different output
console.log(`Output from netbird up:\n${output}`);
// Check if already connected
const statusResponse = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
if (statusResponse.exitCode === 0) {
console.log("\nNetBird status:");
console.log(statusResponse.result);
}
}
// Final status check
console.log("\nFinal NetBird status:");
response = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
console.log(response.result);
}
// Run the main function
setupNetbirdVpnInteractive().catch(console.error);
```
# Preview
Daytona provides preview URLs for accessing services running in your sandboxes. Any process listening for HTTP traffic on ports `1` - `65535` can be previewed through a generated URL.
Daytona supports two types of preview URLs, each with a different authentication mechanism:
- [Standard preview URL](#standard-preview-url) uses the sandbox ID in the URL and requires a separate token for authentication
- [Signed preview URL](#signed-preview-url) embeds the authentication token directly in the URL, requiring no headers
## Authentication
If a sandbox has its `public` property set to `true`, preview links are publicly accessible without authentication. Otherwise, authentication is required. The authentication mechanism depends on the preview URL type.
:::note
Standard and signed preview tokens are not interchangeable. The token from `get_preview_link()` is used as a preview access token (sent via the `x-daytona-preview-token` header). The token from `create_signed_preview_url()` is embedded in the URL itself: it cannot be used as a standard preview token, and vice versa.
:::
## Standard preview URL
The standard preview URL includes your sandbox ID in the URL and provides a separate token for authentication.
URL structure: `https://{port}-{sandboxId}.{daytonaProxyDomain}`
The token resets automatically when the sandbox restarts. Any previously issued standard preview tokens become invalid. Call the `get_preview_link()` method again after starting the sandbox to obtain a fresh token. Use standard preview URLs for programmatic access and API integrations where you control the HTTP headers.
```python
preview_info = sandbox.get_preview_link(3000)
print(f"URL: {preview_info.url}")
print(f"Token: {preview_info.token}")
# Use with requests
import requests
response = requests.get(
preview_info.url,
headers={"x-daytona-preview-token": preview_info.token}
)
```
```typescript
const previewInfo = await sandbox.getPreviewLink(3000);
console.log(`URL: ${previewInfo.url}`);
console.log(`Token: ${previewInfo.token}`);
// Use with fetch
const response = await fetch(previewInfo.url, {
headers: { 'x-daytona-preview-token': previewInfo.token }
});
```
```ruby
preview_info = sandbox.preview_url(3000)
puts "Preview link url: #{preview_info.url}"
puts "Preview link token: #{preview_info.token}"
```
```go
preview, err := sandbox.GetPreviewLink(ctx, 3000)
if err != nil {
log.Fatal(err)
}
fmt.Printf("URL: %s\n", preview.URL)
fmt.Printf("Token: %s\n", preview.Token)
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/3000/preview-url' \
--header 'Authorization: Bearer '
```
### Authentication
Authenticate by sending the token in the `x-daytona-preview-token` header:
```bash
curl -H "x-daytona-preview-token: vg5c0ylmcimr8b_v1ne0u6mdnvit6gc0" \
https://3000-sandbox-123456.proxy.daytona.work
```
## Signed preview URL
The signed preview URL embeds the authentication token directly in the URL, eliminating the need for separate headers. The token persists across sandbox restarts until it expires, or is revoked manually before expiry. Set a custom expiry time for the token:
- Default: `60` seconds
- Minimum: `1` second
- Maximum: `86,400` seconds (24 hours)
- Recommended: `3600` seconds (1 hour)
:::tip
Always set the `expires_in_seconds` parameter explicitly. The default of 60 seconds is short due to security considerations. Most use cases should use at least 3600 (1 hour).
:::
URL structure: `https://{port}-{token}.{daytonaProxyDomain}`
Use signed preview URLs when sharing links with users who cannot set custom headers, embedding previews in iframes or emails, or creating time-limited shareable links.
```python
# Create a signed preview URL that expires in 3600 seconds (1 hour)
signed_url = sandbox.create_signed_preview_url(3000, expires_in_seconds=3600)
print(f"URL: {signed_url.url}") # Token is embedded in the URL
print(f"Token: {signed_url.token}") # Can be used to revoke access
# Use directly - no headers needed
import requests
response = requests.get(signed_url.url)
# Revoke the token before expiry if needed
sandbox.expire_signed_preview_url(3000, signed_url.token)
```
```typescript
// Create a signed preview URL that expires in 3600 seconds (1 hour)
const signedUrl = await sandbox.getSignedPreviewUrl(3000, 3600);
console.log(`URL: ${signedUrl.url}`); // Token is embedded in the URL
console.log(`Token: ${signedUrl.token}`); // Can be used to revoke access
// Use directly - no headers needed
const response = await fetch(signedUrl.url);
// Revoke the token before expiry if needed
await sandbox.expireSignedPreviewUrl(3000, signedUrl.token);
```
```ruby
# Create a signed preview URL that expires in 3600 seconds (1 hour)
signed_url = sandbox.create_signed_preview_url(3000, 3600)
puts "URL: #{signed_url.url}"
puts "Token: #{signed_url.token}"
```
```go
// Create a signed preview URL that expires in 3600 seconds (1 hour)
signedPreview, err := sandbox.GetSignedPreviewLink(ctx, 3000, 3600)
if err != nil {
log.Fatal(err)
}
fmt.Printf("URL: %s\n", signedPreview.URL) // Token is embedded in the URL
fmt.Printf("Token: %s\n", signedPreview.Token) // Can be used to revoke access
// Revoke the token before expiry if needed
if err := sandbox.ExpireSignedPreviewLink(ctx, 3000, signedPreview.Token); err != nil {
log.Fatal(err)
}
```
```bash
daytona preview-url --port 3000 --expires 3600
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/3000/signed-preview-url?expiresInSeconds=3600' \
--header 'Authorization: Bearer '
```
### Authentication
The token is embedded in the URL itself, so no additional headers are required:
```bash
curl https://3000-.proxy.daytona.work
```
:::tip
Port `22222` is used by the [web terminal](https://www.daytona.io/docs/en/web-terminal.md) to access the terminal using preview URLs.
:::
## Warning page
When opening a preview link in a browser for the first time, Daytona displays a warning page. This warning informs users about potential risks of visiting the preview URL and only appears when loading the link in a browser.
To skip the warning page:
- Send the `X-Daytona-Skip-Preview-Warning: true` header
- Upgrade to [Tier 3](https://www.daytona.io/docs/en/limits.md)
- Deploy a [custom preview proxy](https://www.daytona.io/docs/en/custom-preview-proxy.md)
# Custom Preview Proxy
Daytona provides a preview proxy service that can be used to handle [preview URLs](https://www.daytona.io/docs/en/preview.md) for sandboxes. This gives you full control over the preview experience, including custom domains, authentication, error handling, and styling.
- **Custom domain**: host your proxy under your own domain (e.g., `preview.yourcompany.com`)
- **User authentication**: implement custom authentication logic for private previews
- **Sandbox management**: automatically start stopped sandboxes before forwarding users
- **Custom error pages**: style error pages to match your brand
- **Preview warning control**: disable Daytona's preview warning
- **CORS management**: override Daytona's default CORS settings
## How it works
When a user visits a preview URL, your custom proxy receives the request and can:
1. Authenticate the user using custom logic
2. Check sandbox status and start it if needed
3. Forward the request to the actual sandbox
4. Handle responses with custom styling and error pages
5. Send custom headers to control Daytona's behavior
Your proxy should forward the `X-Forwarded-Host` header with the original request host when proxying requests to Daytona.
## WebSocket support
The preview proxy fully supports WebSocket connections. WebSocket upgrade requests (`Upgrade: websocket`) are automatically detected and proxied. WebSocket connections skip the preview warning page.
## Reserved ports
The following ports are reserved for internal services and always require authentication, even on public sandboxes:
| Port | Service |
| ----------- | ----------------------------------------- |
| **`22222`** | [**Web** terminal](https://www.daytona.io/docs/en/web-terminal.md) |
| **`2280`** | Toolbox (IDE/development interface) |
| **`33333`** | Recording dashboard |
Your custom proxy should avoid exposing these ports unless you explicitly need access to these services.
## Proxy headers
Your proxy can send special headers to control Daytona's behavior.
### Disable preview warning
To disable Daytona's preview warning page, send:
```
X-Daytona-Skip-Preview-Warning: true
```
The warning page is only shown to browser requests. It sets a `daytona-preview-page-accepted` cookie that persists for 24 hours after acceptance.
### Disable CORS
Daytona's default CORS policy allows all origins with credentials. To override this and use your own CORS settings, send:
```
X-Daytona-Disable-CORS: true
```
### Disable last activity update
To prevent sandbox last activity updates when previewing, set the `X-Daytona-Skip-Last-Activity-Update` header to `true`. This prevents Daytona from keeping sandboxes that have [auto-stop enabled](https://www.daytona.io/docs/en/sandboxes.md#auto-stop-interval) in a started state:
```bash
curl -H "X-Daytona-Skip-Last-Activity-Update: true" \
https://3000-sandbox-123456.proxy.daytona.work
```
### Authentication
For private preview links, send:
```
X-Daytona-Preview-Token: {sandboxToken}
```
The `sandboxToken` can be fetched through the Daytona SDK or API using the [standard preview URL](https://www.daytona.io/docs/en/preview.md#standard-preview-url) methods.
## Examples
Examples of custom preview proxies are available on [GitHub](https://github.com/daytonaio/daytona-proxy-samples).
# Playground
Daytona provides an interactive environment ("Playground") for creating sandboxes, running SDK operations, and exploring Daytona features directly from your browser.
- **Interactive sandbox creation**: configure and create sandboxes with custom resources
- **Code snippet generation**: automatically generate SDK code samples based on the values configured in the [management](#management) section
- [Web terminal](https://www.daytona.io/docs/en/web-terminal.md): run commands directly in your sandbox through a built-in web terminal
- [VNC access](https://www.daytona.io/docs/en/vnc-access.md): interact with GUI applications using [Computer Use](https://www.daytona.io/docs/en/computer-use.md) features
## Access from Dashboard
Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard/playground) to access the playground.
Playground consists of three tabs: [Sandbox](#sandbox), [Terminal](#terminal), and [VNC](#vnc). All three tabs operate on the same active sandbox.
## Sandbox
The sandbox tab provides an interactive interface to configure sandboxes and perform SDK operations. The left panel contains configurable parameters organized into collapsible sections. The right panel displays auto-generated code snippets that you can inspect, copy, and run.
### Management
The management section contains the parameters used to configure a sandbox. You can edit these parameters to customize your sandbox and its resources.
- [sandbox language](https://www.daytona.io/docs/en/sandboxes.md#multiple-runtime-support): select a programming language runtime for the sandbox
- [sandbox resources](https://www.daytona.io/docs/en/sandboxes.md#resources): configure resources allocated to the sandbox
- [sandbox lifecycle](https://www.daytona.io/docs/en/sandboxes.md#sandbox-lifecycle): set the sandbox lifecycle policies
:::note
In the management section, sandboxes are only created when you explicitly click **Run**, replacing any existing playground sandbox. In the [Terminal](#terminal) or [VNC](#vnc) tabs, if there are no active sandboxes, the playground automatically creates one using the configured parameters.
:::
### File system
The file system section provides operations to manage files and directories in the sandbox. You can modify files and directories in the sandbox using the following operations:
- **Create a new directory**: set the directory location and its permissions
- **List files and directories**: set the directory location
- **Delete files**: set the file location and delete directory checkbox
For more information, see [file system operations](https://www.daytona.io/docs/en/file-system-operations.md).
### Git operations
The Git operations section provides operations to manage Git repositories in the sandbox. You can clone, get repository status, and list branches using the following operations:
- **Clone a Git repository**: set the repository URL, destination, branch, commit ID, and credentials
- **Get repository status**: retrieve the repository status
- **List branches**: retrieve the list of branches in the repository
For more information, see [Git operations](https://www.daytona.io/docs/en/git-operations.md).
### Process and code execution
The process and code execution section provides operations to run code snippets and shell commands directly in the sandbox. Shell commands are fixed, while code snippets change automatically based on the selected [sandbox language](https://www.daytona.io/docs/en/sandboxes.md#multiple-runtime-support) parameter.
For more information, see [process and code execution](https://www.daytona.io/docs/en/process-code-execution.md).
## Terminal
The terminal tab provides a web-based terminal connected to the sandbox. This gives you direct command-line access to the sandbox environment for running commands, viewing files, and debugging.
The terminal runs on port `22222` and remains active as long as the sandbox is running. If the sandbox stops due to inactivity, start it again to restore terminal access.
:::note
Terminal access is restricted to authenticated members of your [organization](https://www.daytona.io/docs/en/organizations.md).
:::
For more information, see [web terminal](https://www.daytona.io/docs/en/web-terminal.md).
## VNC
The VNC tab provides graphical desktop access to the sandbox, enabling you to interact with GUI applications and test [Computer Use](https://www.daytona.io/docs/en/computer-use.md) features. The left panel contains interaction controls organized into sections. The right panel displays the desktop view.
For more information, see [VNC access](https://www.daytona.io/docs/en/vnc-access.md).
### Display
The display section provides options to inspect the sandbox desktop environment.
- **Get display information**: retrieve information about the available displays
- **List open windows**: retrieve a list of currently open windows
For more information, see [display operations](https://www.daytona.io/docs/en/computer-use.md#display-operations).
### Keyboard
The keyboard section provides options to send keyboard input to the sandbox.
- **Press a hotkey combination**: send a hotkey combination to the sandbox
- **Press a key**: press a key with optional modifiers
- **Type text**: type text into the active window
For more information, see [keyboard operations](https://www.daytona.io/docs/en/computer-use.md#keyboard-operations).
### Mouse
The mouse section provides options to control mouse input in the sandbox.
- **Click**: click at a specified position
- **Drag**: drag from one position to another
- **Move**: move the cursor to a specified position
- **Get cursor position**: retrieve the current cursor position
For more information, see [mouse operations](https://www.daytona.io/docs/en/computer-use.md#mouse-operations).
### Screenshot
The screenshot section provides options to capture screenshots of the sandbox desktop.
- **Take a screenshot**: select format, scale, and quality, show cursor, and capture the entire screen or specific regions
For more information, see [screenshot operations](https://www.daytona.io/docs/en/computer-use.md#screenshot-operations).
## Related
- [Web Terminal](https://www.daytona.io/docs/en/web-terminal.md): browser-based terminal access for sandboxes
- [VNC Access](https://www.daytona.io/docs/en/vnc-access.md): graphical desktop access for sandboxes
- [Computer Use](https://www.daytona.io/docs/en/computer-use.md): programmatic desktop automation
- [File System Operations](https://www.daytona.io/docs/en/file-system-operations.md): manage files and directories in sandboxes
- [Git Operations](https://www.daytona.io/docs/en/git-operations.md): manage Git repositories in sandboxes
- [Process & Code Execution](https://www.daytona.io/docs/en/process-code-execution.md): execute commands and run code in sandboxes
# Webhooks
Webhooks are HTTP callbacks that Daytona sends to your specified endpoints when specific events occur.
Think of them as "reverse API calls" - instead of your application asking Daytona for updates, Daytona
proactively notifies your application when something important happens.
Webhooks enable powerful automation and integration scenarios:
- **Real-time notifications**: get instant alerts when sandboxes are created, started, or stopped
- **Automated workflows**: trigger deployment pipelines when snapshots are created
- **Monitoring & analytics**: track usage patterns and resource utilization across organizations
- **Integration**: connect Daytona with existing tools like Slack, Discord, or custom applications
- **Audit & compliance**: maintain detailed logs of all important changes
:::note
Webhooks are available to organization admins and members with appropriate permissions. If you don't see **Webhooks** in [Daytona Dashboard ↗](https://app.daytona.io/dashboard), contact [support@daytona.io](mailto:support@daytona.io) to enable webhooks for your organization.
:::
## Accessing webhooks
Daytona provides a webhook management interface to access and manage webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
The webhooks management interface contains two tabs: [Endpoints](#endpoints) and [Messages](#messages).
### Endpoints
The endpoints tab lists all the webhook endpoints for the organization. Each endpoint has the following details:
- **Name**: the name of the webhook endpoint
- **URL**: the URL of the webhook endpoint
- **Status**: the status of the webhook endpoint
- **Created**: the timestamp when the webhook endpoint was created
Clicking on an endpoint opens the endpoint details page with additional endpoint configuration details: **name**, **URL**, **events**, **status**, **delivery statistics**, and **event history**.
### Messages
The messages tab lists all the webhook messages for the organization. Each message has the following details:
- **Message ID**: the ID of the webhook message
- **Event type**: the event type of the webhook message
- **Timestamp**: the timestamp when the webhook message was created
Clicking on a message opens the message details dialog with additional message details: **message ID**, **event type**, **timestamp**, **payload**, and **delivery attempts**.
Delivery attempts are shown in a separate table with the following columns:
- **Status**: the status of the delivery attempt
- **URL**: the URL of the delivery attempt
- **Timestamp**: the timestamp when the delivery attempt was created
Expanding a delivery attempt displays additional delivery attempt details: **status code**, **duration**, **trigger**, **endpoint ID**, and **response body**.
## Create webhook endpoints
Daytona provides a webhook management interface to create webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Click **Add Endpoint**
4. Configure your endpoint:
- **Endpoint name**: a name for the endpoint
- **Endpoint URL**: HTTPS endpoint where you want to receive events
- [Events](#webhook-events): select which events to subscribe to
5. Click **Create**
## Edit webhook endpoints
Daytona provides a webhook management interface to edit webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Select a webhook endpoint from the [Endpoints](#endpoints) tab
4. Click the three dots menu (**⋮**) and select **Edit**
5. Update the endpoint details
6. Click **Save**
## Delete webhook endpoints
Daytona provides a webhook management interface to delete webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Select a webhook endpoint from the [Endpoints](#endpoints) tab
4. Click the three dots menu (**⋮**) and select **Delete**
5. Confirm the deletion
## Webhook events
Daytona sends webhooks for lifecycle events across your infrastructure resources. You can subscribe to specific event types or receive all events and filter them in your application.
### Sandbox events
| Event Type | Description |
| --------------------------- | ------------------------------ |
| **`sandbox.created`** | A new sandbox has been created |
| **`sandbox.state.updated`** | A sandbox's state has changed |
### Snapshot events
| Event Type | Description |
| ---------------------------- | ------------------------------- |
| **`snapshot.created`** | A new snapshot has been created |
| **`snapshot.state.updated`** | A snapshot's state has changed |
| **`snapshot.removed`** | A snapshot has been removed |
### Volume events
| Event Type | Description |
| -------------------------- | ----------------------------- |
| **`volume.created`** | A new volume has been created |
| **`volume.state.updated`** | A volume's state has changed |
## Webhook payload format
All webhook payloads are JSON objects following a consistent format with common fields and event-specific data.
**Common Fields:**
| Field | Type | Description |
| --------------- | ------ | ----------------------------------------------- |
| **`event`** | string | Event type identifier (e.g., `sandbox.created`) |
| **`timestamp`** | string | ISO 8601 timestamp when the event occurred |
### **`sandbox.created`**
Sent when a new sandbox is created.
```json
{
"event": "sandbox.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "sandbox123",
"organizationId": "org123",
"state": "started",
"sandboxClass": "container",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ----------------------------------------------- |
| **`id`** | string | Sandbox ID |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Sandbox state |
| **`sandboxClass`** | string | Sandbox class |
| **`createdAt`** | string | ISO 8601 timestamp when the sandbox was created |
### **`sandbox.state.updated`**
Sent when a sandbox's state changes.
```json
{
"event": "sandbox.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "sandbox123",
"organizationId": "org123",
"oldState": "started",
"newState": "stopped",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ---------------------------------------------------- |
| **`id`** | string | Sandbox ID |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous sandbox state |
| **`newState`** | string | New sandbox state |
| **`updatedAt`** | string | ISO 8601 timestamp when the sandbox was last updated |
### **`snapshot.created`**
Sent when a new snapshot is created.
```json
{
"event": "snapshot.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"state": "active",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ------------------------------------------------ |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Snapshot state |
| **`createdAt`** | string | ISO 8601 timestamp when the snapshot was created |
### **`snapshot.state.updated`**
Sent when a snapshot's state changes.
```json
{
"event": "snapshot.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"oldState": "building",
"newState": "active",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ----------------------------------------------------- |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous snapshot state |
| **`newState`** | string | New snapshot state |
| **`updatedAt`** | string | ISO 8601 timestamp when the snapshot was last updated |
### **`snapshot.removed`**
Sent when a snapshot is removed.
```json
{
"event": "snapshot.removed",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"removedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ------------------------------------------------ |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`removedAt`** | string | ISO 8601 timestamp when the snapshot was removed |
### **`volume.created`**
Sent when a new volume is created.
```json
{
"event": "volume.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "vol-12345678",
"name": "my-volume",
"organizationId": "org123",
"state": "ready",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ---------------------------------------------- |
| **`id`** | string | Volume ID |
| **`name`** | string | Volume name |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Volume state |
| **`createdAt`** | string | ISO 8601 timestamp when the volume was created |
### **`volume.state.updated`**
Sent when a volume's state changes.
```json
{
"event": "volume.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "vol-12345678",
"name": "my-volume",
"organizationId": "org123",
"oldState": "creating",
"newState": "ready",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | --------------------------------------------------- |
| **`id`** | string | Volume ID |
| **`name`** | string | Volume name |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous volume state |
| **`newState`** | string | New volume state |
| **`updatedAt`** | string | ISO 8601 timestamp when the volume was last updated |
# Network Limits (Firewall)
Daytona provides network egress limiting for sandboxes to control internet access. This feature can be automatically applied based on your [organization's limits](https://www.daytona.io/docs/en/limits.md) or manually configured for specific sandboxes.
## Tier-based network restrictions
Network limits are automatically applied to sandboxes based on your organization's billing tier. This provides secure and controlled internet access for development environments:
- **Tier 1 & Tier 2**: Network access is restricted and cannot be overridden at the sandbox level. Organization-level network restrictions take precedence over sandbox-level settings. Even with [`networkAllowList`](#create-sandboxes-with-network-restrictions) specified when creating a sandbox, the organization's network restrictions still apply
- **Tier 3 & Tier 4**: Full internet access is available by default, with the ability to configure custom network settings
> To learn more about organization tiers and limits, see [limits](https://www.daytona.io/docs/en/limits.md).
[Essential services](#essential-services) are available on all tiers and include services essential for development: package registries, container registries, Git repositories, CDN services, platform services, and system package managers.
## Create sandboxes with network restrictions
Daytona provides methods to control network access when [creating sandboxes](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes) by using the `networkAllowList` and `networkBlockAll` parameters:
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
network_allow_list='208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
))
# Or block all network access
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
network_block_all=True
))
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
const sandbox = await daytona.create({
networkAllowList: '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
})
// Or block all network access
const sandbox = await daytona.create({
networkBlockAll: true
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
network_allow_list: '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
)
)
# Or block all network access
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
network_block_all: true
)
)
```
```go
package main
import (
"context"
"log"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/daytona"
"github.com/daytonaio/daytona/libs/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
allowList := "208.80.154.232/32,199.16.156.103/32,192.168.1.0/24"
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
NetworkAllowList: &allowList,
},
})
// Or block all network access
sandbox, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
NetworkBlockAll: true,
},
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// Or block all network access
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setNetworkBlockAll(true);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"networkAllowList": "208.80.154.232/32,199.16.156.103/32,192.168.1.0/24"
}'
# Or block all network access
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"networkBlockAll": true
}'
```
```bash
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
daytona create --network-allow-list '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
# Or block all network access
daytona create --network-block-all
```
:::note
If both `networkBlockAll` and `networkAllowList` are specified, `networkBlockAll` takes precedence and all network access will be blocked, ignoring the allow list.
:::
## Update network settings while a sandbox is running
Daytona provides methods to update network settings for running sandboxes. Organizations on [Tier 3 and Tier 4](#tier-based-network-restrictions) can change outbound firewall policy after the sandbox is created. The API applies the new rules on the runner and persists them on the sandbox record. The sandbox keeps running; stop or start are not required.
The request must include at least one of `networkBlockAll` or `networkAllowList`. Rules match create-time behavior and use the same [allow list format](#network-allow-list-format).
- Sending `networkAllowList` as an empty string clears a stored allow list
- Sending `networkBlockAll: true` blocks all outbound traffic and clears the allow list
- Sending only `networkBlockAll: false` restores general outbound access (for your tier) and clears a stored allow list
This operation requires the `WRITE_SANDBOXES` permission. Organizations on Tier 1 or Tier 2 cannot override network policy at the sandbox level, and the API returns an error in that case.
```python
# Block all outbound traffic (clears any allow list)
sandbox.update_network_settings(network_block_all=True)
# Restore general outbound access and clear the allow list
sandbox.update_network_settings(network_block_all=False)
# Apply or replace a CIDR allow list (implies not blocking all)
sandbox.update_network_settings(
network_allow_list='208.80.154.232/32,192.168.1.0/24'
)
# Clear a stored allow list (empty string). Outbound traffic still follows `network_block_all`.
sandbox.update_network_settings(network_allow_list='')
```
```typescript
// Block all outbound traffic (clears any allow list)
await sandbox.updateNetworkSettings({ networkBlockAll: true })
// Restore general outbound access and clear the allow list
await sandbox.updateNetworkSettings({ networkBlockAll: false })
// Apply or replace a CIDR allow list (implies not blocking all)
await sandbox.updateNetworkSettings({
networkAllowList: '208.80.154.232/32,192.168.1.0/24',
})
// Clear a stored allow list (empty string). Outbound traffic still follows `networkBlockAll`.
await sandbox.updateNetworkSettings({ networkAllowList: '' })
```
```ruby
# Block all outbound traffic (clears any allow list)
sandbox.update_network_settings(network_block_all: true)
# Restore general outbound access and clear the allow list
sandbox.update_network_settings(network_block_all: false)
# Apply or replace a CIDR allow list (implies not blocking all)
sandbox.update_network_settings(
network_allow_list: '208.80.154.232/32,192.168.1.0/24'
)
# Clear the allow list (empty string)
sandbox.update_network_settings(network_allow_list: '')
```
```go
import apiclient "github.com/daytonaio/daytona/libs/api-client-go"
settings := apiclient.NewUpdateSandboxNetworkSettings()
settings.SetNetworkBlockAll(true)
if err := sandbox.UpdateNetworkSettings(ctx, *settings); err != nil {
log.Fatal(err)
}
restore := apiclient.NewUpdateSandboxNetworkSettings()
restore.SetNetworkBlockAll(false)
if err := sandbox.UpdateNetworkSettings(ctx, *restore); err != nil {
log.Fatal(err)
}
allow := apiclient.NewUpdateSandboxNetworkSettings()
allow.SetNetworkAllowList("208.80.154.232/32,192.168.1.0/24")
if err := sandbox.UpdateNetworkSettings(ctx, *allow); err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.api.client.model.UpdateSandboxNetworkSettings;
// Block all outbound traffic (clears any allow list)
sandbox.updateNetworkSettings(new UpdateSandboxNetworkSettings().networkBlockAll(true));
// Restore general outbound access and clear the allow list
sandbox.updateNetworkSettings(new UpdateSandboxNetworkSettings().networkBlockAll(false));
// Apply or replace a CIDR allow list
sandbox.updateNetworkSettings(
new UpdateSandboxNetworkSettings().networkAllowList("208.80.154.232/32,192.168.1.0/24"));
```
```bash
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"networkBlockAll": true}'
# Restore access and clear allow list
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"networkBlockAll": false}'
```
## Network allow list format
The network allow list is a comma-separated list of IPv4 CIDR blocks. Set your allowed networks using the `networkAllowList` parameter when [creating a sandbox](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes) or when [updating settings on a running sandbox](#update-network-settings-while-a-sandbox-is-running).
- **IPv4 only**: hostnames, domains, and IPv6 are not supported
- **CIDR required**: every entry must include a `/` prefix length integer in the range `0` to `32` (inclusive), for example: `/32`
- **CIDR format**: use standard CIDR notation (`A.B.C.D/N`). Do not include extra `/` segments
- **Max 10 entries**: the list cannot contain more than 10 comma-separated items
- **Whitespace is ignored**: entries are trimmed, so spaces around commas are ok
The following examples are valid:
- **Single IP**: `208.80.154.232/32` (Wikipedia)
- **Subnet**: `192.168.1.0/24` (Private network)
- **Multiple networks**: `208.80.154.232/32,199.16.156.103/32,10.0.0.0/8`
## Organization configuration
The network access policies for your organization are set automatically depending on your organization's limits tier and cannot be modified by organization administrators. These policies determine the default network behavior for all sandboxes in your organization.
## Test network access
To test network connectivity from your sandbox:
```bash
# Test HTTP connectivity to allowed addresses
curl -I https://208.80.154.232
# Test package manager access (allowed on all tiers)
apt update # For Ubuntu/Debian
npm ping # For Node.js
pip install --dry-run requests # For Python
```
## Security benefits
Network limits provide several security advantages:
- **Prevents data exfiltration** from sandboxes
- **Reduces attack surface** by limiting external connections
- **Complies with security policies** for development environments
- **Enables fine-grained control** over network access
:::caution
Enabling unrestricted network access may pose security risks when executing untrusted code. It is recommended to whitelist specific network addresses using `networkAllowList` or block all network access using `networkBlockAll` instead.
Test network connectivity before starting critical development work and consider upgrading your tier if you need access to many external services.
:::
## Essential services
Daytona provides a list of essential services that are available on all tiers and can be used for development.
:::note
This list is continuously updated. If you require access to additional essential development services, submit a request in the [sandbox network whitelist](https://github.com/daytonaio/sandbox-network-whitelist) repository or contact [support@daytona.io](mailto:support@daytona.io).
:::
### NPM registry and package managers
- **NPM Registry**: `registry.npmjs.org`, `registry.npmjs.com`, `nodejs.org`, `nodesource.com`, `npm.pkg.github.com`
- **Yarn Packages**: `yarnpkg.com`, `*.yarnpkg.com`, `yarn.npmjs.org`, `yarnpkg.netlify.com`
- **Bun**: `bun.sh`, `*.bun.sh`
### Git hosting and version control
- **GitHub**: `github.com`, `*.github.com`, `*.githubusercontent.com`, `ghcr.io`
- **GitLab**: `gitlab.com`, `*.gitlab.com`
- **Bitbucket**: `bitbucket.org`
- **Azure DevOps**: `dev.azure.com`, `*.dev.azure.com`, `login.microsoftonline.com`, `visualstudio.com`, `*.visualstudio.com`, `ssh.dev.azure.com`, `vs-ssh.visualstudio.com`
### Python package managers
- **PyPI**: `pypi.org`, `pypi.python.org`, `files.pythonhosted.org`, `bootstrap.pypa.io`, `astral.sh`
### Composer packages
- **Composer**: `*.packagist.org`, `packagist.com`
### Ubuntu/Debian package repositories
- **Ubuntu Repos**: `*.ubuntu.com`
- **Debian Repos**: `*.debian.org`, `cdn-fastly.deb.debian.org`
### CDN and content delivery
- **CDN Services**: `fastly.com`, `cloudflare.com`, `r2.cloudflarestorage.com`, `*.r2.cloudflarestorage.com`
- **JavaScript CDNs**: `unpkg.com`, `jsdelivr.net`
### AI/ML services
- **Anthropic**: `*.anthropic.com`, `claude.ai`, `platform.claude.com`
- **OpenAI**: `openai.com`, `*.openai.com`, `chatgpt.com`
- **Google AI**: `generativelanguage.googleapis.com`, `gemini.google.com`, `aistudio.google.com`, `ai.google.dev`, `models.dev`
- **Perplexity**: `api.perplexity.ai`
- **DeepSeek**: `api.deepseek.com`
- **Groq**: `api.groq.com`
- **Expo**: `api.expo.dev`
- **OpenRouter**: `openrouter.ai`
- **Qwen**: `chat.qwen.ai`, `dashscope.aliyuncs.com`, `dashscope-intl.aliyuncs.com`
- **Cursor**: `*.cursor.com`
- **OpenCode**: `opencode.ai`, `*.opencode.ai`
- **Other AI Services**: `api.letta.com`, `api.fireworks.ai`, `open.bigmodel.cn`, `*.z.ai`, `*.moonshot.ai`, `ai-gateway.vercel.sh`, `api.featherless.ai`
### Docker registries and container services
- **Docker Registries**: `docker.io`, `*.docker.io`, `*.docker.com`
- **Microsoft Container Registry**: `mcr.microsoft.com`
- **Kubernetes Registry**: `registry.k8s.io`
- **Google Container Registry**: `gcr.io`, `*.gcr.io`, `registry.cloud.google.com`
- **Quay**: `quay.io`, `quay-registry.s3.amazonaws.com`
### Maven repositories
- **Maven Repos**: `repo1.maven.org`, `repo.maven.apache.org`
### Google Fonts
- **Google Fonts**: `fonts.googleapis.com`, `fonts.gstatic.com`
### AWS S3 endpoints
- **US East**: `s3.us-east-1.amazonaws.com`, `s3.us-east-2.amazonaws.com`
- **US West**: `s3.us-west-1.amazonaws.com`, `s3.us-west-2.amazonaws.com`
- **EU**: `s3.eu-central-1.amazonaws.com`, `s3.eu-west-1.amazonaws.com`, `s3.eu-west-2.amazonaws.com`
### Google Cloud Storage
- **GCS**: `storage.googleapis.com`
### Daytona
- **Daytona**: `app.daytona.io`
### Developer tools and services
- **Convex**: `convex.dev`, `*.convex.dev`, `*.convex.cloud`, `*.convex.site`
- **Heroku**: `herokuapp.com`, `*.herokuapp.com`
- **Vercel**: `vercel.com`, `*.vercel.com`, `*.vercel.app`
- **Supabase**: `supabase.com`, `*.supabase.com`, `supabase.co`, `*.supabase.co`
- **Clerk**: `clerk.com`, `*.clerk.com`, `clerk.dev`, `*.clerk.dev`, `accounts.dev`, `*.accounts.dev`, `clerk.accounts.dev`, `*.clerk.accounts.dev`
- **WorkOS**: `workos.com`, `*.workos.com`, `authkit.app`, `*.authkit.app`
- **Inngest**: `inngest.com`, `*.inngest.com`
- **PostHog**: `posthog.com`, `*.posthog.com`
- **Sentry**: `sentry.io`, `*.sentry.io`, `sentry-cdn.com`, `*.sentry-cdn.com`
- **Linear**: `linear.app`, `*.linear.app`
- **Figma**: `figma.com`, `*.figma.com`, `*.figmafiles.com`
- **ClickUp**: `clickup.com`, `*.clickup.com`
- **Playwright**: `playwright.dev`, `cdn.playwright.dev`
### Messaging services
- **Telegram**: `api.telegram.org`
- **WhatsApp**: `web.whatsapp.com`, `*.whatsapp.net`
### LLM observability
- **Langfuse**: `*.langfuse.com`, `*.cloud.langfuse.com`
## Troubleshooting
If you encounter network access issues or need unrestricted network access:
1. Verify your [organization tier](https://www.daytona.io/docs/en/limits.md#tiers) in the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/limits)
2. Verify your [network allow list](#network-allow-list-format) configuration
3. Contact [support@daytona.io](mailto:support@daytona.io) for assistance
# Guides
Daytona provides a comprehensive set of guides to help you get started.
The guides cover a wide range of topics, from basic usage to advanced topics, and showcase various types of integrations between Daytona and other tools.
# Analyze Data with AI
import { Image } from 'astro:assets'
import chartImage from '../../../../assets/docs/images/chart-0.png'
You can use Daytona Sandbox to run AI-generated code to analyze data. Here's how the AI data analysis workflow typically looks:
1. Your user has a dataset in CSV format or other formats.
2. You prompt the LLM to generate code (usually Python) based on the user's data.
3. The sandbox runs the AI-generated code and returns the results.
4. The LLM receives feedback from the execution and can iterate multiple times to refine the code if needed.
5. You display the final results to the user.
---
## Build an AI Data Analyst with Daytona
This example shows how to build an AI-powered data analyst that automatically generates insights and visualizations from CSV data using Daytona's secure sandbox environment.
**What we'll build:** A system that analyzes a vehicle valuation dataset, identifies price relation to manufacturing year, and generates professional visualizations - all through natural language prompts to Claude. The system uses an agentic loop that allows Claude to iteratively refine the code based on execution results.
### 1. Project Setup
#### 1.1 Install Dependencies
Install the Daytona SDK and Anthropic SDK to your project:
`bash pip install daytona anthropic python-dotenv `
`bash npm install @daytona/sdk @anthropic-ai/sdk dotenv `
`bash gem install daytona anthropic dotenv `
#### 1.2 Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 2. Dataset Preparation
#### 2.1 Download Dataset
We'll be using a publicly available dataset of vehicle valuation. You can download it directly from:
[https://download.daytona.io/dataset.csv](https://download.daytona.io/dataset.csv)
Download the file and save it as `dataset.csv` in your project directory.
#### 2.2 Initialize Sandbox
Now create a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes) and upload your dataset:
```python
from dotenv import load_dotenv
from daytona import Daytona
import os
load_dotenv()
# Create sandbox
daytona = Daytona() # The sandbox language is Python by default.
sandbox = daytona.create()
# Upload the dataset to the sandbox
sandbox.fs.upload_file("dataset.csv", "/home/daytona/dataset.csv")
```
```typescript
import 'dotenv/config'
import { Daytona } from '@daytona/sdk';
// Create sandbox
const daytona = new Daytona(); // The sandbox language is Python by default.
const sandbox = await daytona.create()
// Upload the dataset to the sandbox
await sandbox.fs.uploadFile('dataset.csv', '/home/daytona/dataset.csv')
```
```ruby
require 'daytona'
require 'dotenv/load'
# Create sandbox
daytona = Daytona::Daytona.new # The sandbox language is Python by default.
sandbox = daytona.create
# Upload the dataset to the sandbox
sandbox.fs.upload_file(File.read('dataset.csv'), '/home/daytona/dataset.csv')
```
### 3. Building the AI Data Analyst
Now we'll create the core functionality that connects Claude with Daytona to analyze data and generate visualizations.
#### 3.1 Code Execution Handler
First, let's create a function to handle code execution and chart extraction. This function returns execution results that can be fed back to the AI model:
```python
import base64
from typing import TypedDict
class ExecutionResult(TypedDict):
stdout: str
exit_code: int
charts: list
def run_ai_generated_code(sandbox, ai_generated_code: str) -> ExecutionResult:
execution = sandbox.process.code_run(ai_generated_code)
result = ExecutionResult(
stdout=execution.result or "",
exit_code=execution.exit_code,
charts=execution.artifacts.charts if execution.artifacts else []
)
# Save any charts that were generated
if execution.artifacts and execution.artifacts.charts:
result_idx = 0
for chart in execution.artifacts.charts:
if chart.png:
filename = f'chart-{result_idx}.png'
with open(filename, 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'✓ Chart saved to {filename}')
result_idx += 1
return result
```
```typescript
import fs from 'fs'
import { Sandbox } from '@daytona/sdk'
interface ExecutionResult {
stdout: string
exitCode: number
charts?: Array<{ png?: string }>
}
async function runAIGeneratedCode(
sandbox: Sandbox,
aiGeneratedCode: string
): Promise {
const execution = await sandbox.process.codeRun(aiGeneratedCode)
const result: ExecutionResult = {
stdout: execution.result || "",
exitCode: execution.exitCode,
charts: execution.artifacts?.charts
}
// Save any charts that were generated
if (execution.artifacts?.charts) {
let resultIdx = 0
for (const chart of execution.artifacts.charts) {
if (chart.png) {
const filename = `chart-${resultIdx}.png`
fs.writeFileSync(filename, chart.png, { encoding: 'base64' })
console.log(`✓ Chart saved to ${filename}`)
resultIdx++
}
}
}
return result
}
```
#### 3.2 Creating the Analysis Prompt
Next, we'll create the prompt that tells Claude about our dataset and what analysis we want. This prompt includes:
- Dataset schema and column descriptions
- The specific analysis request (vehicle price variation by manufacturing year)
- Instructions for code generation
```python
from anthropic import Anthropic
prompt = f"""
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart."""
anthropic = Anthropic()
```
```typescript
import Anthropic from '@anthropic-ai/sdk'
const prompt = `
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart.`
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
```
#### 3.3 Tool Definition
Define the tool that allows Claude to execute Python code in the sandbox:
```python
tools = [
{
'name': 'run_python_code',
'description': 'Run Python code in the sandbox environment and get execution results',
'input_schema': {
'type': 'object',
'properties': {
'code': {
'type': 'string',
'description': 'The Python code to run',
},
},
'required': ['code'],
},
},
]
```
```typescript
import type { Tool, ToolUseBlock } from '@anthropic-ai/sdk/resources/messages.mjs'
const tools: Tool[] = [
{
name: 'run_python_code',
description: 'Run Python code in the sandbox environment and get execution results',
input_schema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'The Python code to run',
},
},
required: ['code'],
},
},
]
```
#### 3.4 Agentic Loop Implementation
Now we'll implement the agentic loop that allows Claude to iteratively refine the code based on execution feedback. This enables Claude to fix errors, handle edge cases, and improve the analysis through multiple iterations:
```python
# Initialize conversation history
messages = [{'role': 'user', 'content': prompt}]
continue_loop = True
iteration_count = 0
max_iterations = 10
print("Starting agentic loop...\n")
while continue_loop and iteration_count < max_iterations:
iteration_count += 1
print(f"\n=== Iteration {iteration_count} ===")
print("Waiting for the model response...")
# Get response from Claude
msg = anthropic.messages.create(
model='claude-sonnet-4-5',
max_tokens=64000,
messages=messages,
tools=tools
)
# Log Claude's text response
for content_block in msg.content:
if content_block.type == 'text':
print("\nClaude's response:")
print(content_block.text)
# Check if Claude wants to use any tools
tool_uses = [block for block in msg.content if block.type == 'tool_use']
if len(tool_uses) == 0:
# No more tool uses, Claude is done
print("\nTask completed - no more actions needed.")
continue_loop = False
break
# Add Claude's response to message history
messages.append({'role': 'assistant', 'content': msg.content})
# Execute all tool calls and collect results
tool_results = []
for tool_use in tool_uses:
if tool_use.name == 'run_python_code':
code = tool_use.input['code']
print("\n--- Executing Python code in sandbox ---")
print(code)
print("--- End of code ---\n")
# Execute the code in the sandbox
execution_result = run_ai_generated_code(sandbox, code)
# Format the tool result
result_content = ""
if execution_result['exit_code'] == 0:
result_content += "Execution successful!\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
if execution_result['charts'] and len(execution_result['charts']) > 0:
result_content += f"\nGenerated {len(execution_result['charts'])} chart(s)."
else:
result_content += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
else:
result_content += f"Execution failed with exit code {execution_result['exit_code']}\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
tool_results.append({
'type': 'tool_result',
'tool_use_id': tool_use.id,
'content': result_content
})
print("Execution result sent back to Claude.")
# Add tool results to conversation history
messages.append({'role': 'user', 'content': tool_results})
if iteration_count >= max_iterations:
print("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
print("\n=== Agentic loop completed ===")
```
```typescript
import type { MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
interface CodeRunToolInput {
code: string
}
// Initialize conversation history
const messages: MessageParam[] = [
{ role: 'user', content: initialPrompt }
]
let continueLoop = true
let iterationCount = 0
const maxIterations = 10
console.log("Starting agentic loop...\n")
while (continueLoop && iterationCount < maxIterations) {
iterationCount++
console.log(`\n=== Iteration ${iterationCount} ===`)
console.log("Waiting for the model response...")
// Get response from Claude
const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-5',
max_tokens: 64000,
messages: messages,
tools: tools
})
const message = await stream.finalMessage()
// Log Claude's text response
for (const contentBlock of message.content) {
if (contentBlock.type === 'text') {
console.log("\nClaude's response:")
console.log(contentBlock.text)
}
}
// Check if Claude wants to use any tools
const toolUses = message.content.filter(
(block): block is ToolUseBlock => block.type === 'tool_use'
)
if (toolUses.length === 0) {
// No more tool uses, Claude is done
console.log("\nTask completed - no more actions needed.")
continueLoop = false
break
}
// Add Claude's response to message history
messages.push({
role: 'assistant',
content: message.content
})
// Execute all tool calls and collect results
const toolResults = []
for (const toolUse of toolUses) {
if (toolUse.name === 'run_python_code') {
const code = (toolUse.input as CodeRunToolInput).code
console.log("\n--- Executing Python code in sandbox ---")
console.log(code)
console.log("--- End of code ---\n")
// Execute the code in the sandbox
const executionResult = await runAIGeneratedCode(sandbox, code)
// Format the tool result
let resultContent = ""
if (executionResult.exitCode === 0) {
resultContent += "Execution successful!\n\n"
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`
}
if (executionResult.charts && executionResult.charts.length > 0) {
resultContent += `\nGenerated ${executionResult.charts.length} chart(s).`
} else {
resultContent += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
}
} else {
resultContent += `Execution failed with exit code ${executionResult.exitCode}\n\n`
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`
}
}
toolResults.push({
type: 'tool_result' as const,
tool_use_id: toolUse.id,
content: resultContent
})
console.log("Execution result sent back to Claude.")
}
}
// Add tool results to conversation history
messages.push({
role: 'user',
content: toolResults
})
}
if (iterationCount >= maxIterations) {
console.log("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
}
console.log("\n=== Agentic loop completed ===")
```
The agentic loop works as follows:
1. **Initial Request**: Send the initial prompt to Claude with the tool definition
2. **Iteration Loop**: For each iteration (up to 10 times):
- Claude generates a response with optional tool calls
- If there are tool calls, execute the Python code in the sandbox
- Send execution results back to Claude (including errors or success messages)
- Claude can then refine the code based on the feedback
3. **Completion**: Loop ends when Claude signals no more tool calls are needed or max iterations reached
This approach allows Claude to:
- Fix errors if the initial code fails
- Iterate on the analysis if results aren't satisfactory
- Handle edge cases discovered during execution
- Improve visualizations based on the actual data
**Key advantages of this approach:**
- **Secure execution:** Code runs in isolated Daytona sandboxes
- **Automatic artifact capture:** Charts, tables, and outputs are automatically extracted
- **Error handling:** Built-in error detection and logging
- **Language agnostic:** While we used Python here, Daytona supports multiple languages
### 4. Running Your Analysis
Now you can run the complete code to see the results.
```bash
python data-analysis.py
```
```bash
npx tsx data-analysis.ts
```
You should see the chart in your project directory that will look similar to this:
### 5. Complete Implementation
Here are the complete, ready-to-run examples with the agentic loop:
```python
import base64
from dotenv import load_dotenv
from daytona import Daytona, Sandbox
from anthropic import Anthropic
from typing import TypedDict
class ExecutionResult(TypedDict):
stdout: str
exit_code: int
charts: list
def main():
load_dotenv()
# Create sandbox
daytona = Daytona()
sandbox = daytona.create()
# Upload the dataset to the sandbox
sandbox.fs.upload_file("dataset.csv", "/home/daytona/dataset.csv")
initial_prompt = """
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart."""
anthropic = Anthropic()
tools = [
{
'name': 'run_python_code',
'description': 'Run Python code in the sandbox environment and get execution results',
'input_schema': {
'type': 'object',
'properties': {
'code': {
'type': 'string',
'description': 'The Python code to run',
},
},
'required': ['code'],
},
},
]
# Initialize conversation history
messages = [{'role': 'user', 'content': initial_prompt}]
continue_loop = True
iteration_count = 0
max_iterations = 10
print("Starting agentic loop...\n")
while continue_loop and iteration_count < max_iterations:
iteration_count += 1
print(f"\n=== Iteration {iteration_count} ===")
print("Waiting for the model response...")
# Get response from Claude
msg = anthropic.messages.create(
model='claude-sonnet-4-5',
max_tokens=64000,
messages=messages,
tools=tools
)
# Log Claude's text response
for content_block in msg.content:
if content_block.type == 'text':
print("\nClaude's response:")
print(content_block.text)
# Check if Claude wants to use any tools
tool_uses = [block for block in msg.content if block.type == 'tool_use']
if len(tool_uses) == 0:
# No more tool uses, Claude is done
print("\nTask completed - no more actions needed.")
continue_loop = False
break
# Add Claude's response to message history
messages.append({'role': 'assistant', 'content': msg.content})
# Execute all tool calls and collect results
tool_results = []
for tool_use in tool_uses:
if tool_use.name == 'run_python_code':
code = tool_use.input['code']
print("\n--- Executing Python code in sandbox ---")
print(code)
print("--- End of code ---\n")
# Execute the code in the sandbox
execution_result = run_ai_generated_code(sandbox, code)
# Format the tool result
result_content = ""
if execution_result['exit_code'] == 0:
result_content += "Execution successful!\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
if execution_result['charts'] and len(execution_result['charts']) > 0:
result_content += f"\nGenerated {len(execution_result['charts'])} chart(s)."
else:
result_content += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
else:
result_content += f"Execution failed with exit code {execution_result['exit_code']}\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
tool_results.append({
'type': 'tool_result',
'tool_use_id': tool_use.id,
'content': result_content
})
print("Execution result sent back to Claude.")
# Add tool results to conversation history
messages.append({'role': 'user', 'content': tool_results})
if iteration_count >= max_iterations:
print("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
print("\n=== Agentic loop completed ===")
def run_ai_generated_code(sandbox: Sandbox, ai_generated_code: str) -> ExecutionResult:
execution = sandbox.process.code_run(ai_generated_code)
result = ExecutionResult(
stdout=execution.result or "",
exit_code=execution.exit_code,
charts=execution.artifacts.charts if execution.artifacts else []
)
# Save any charts that were generated
if execution.artifacts and execution.artifacts.charts:
result_idx = 0
for chart in execution.artifacts.charts:
if chart.png:
filename = f'chart-{result_idx}.png'
with open(filename, 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'✓ Chart saved to {filename}')
result_idx += 1
return result
if __name__ == "__main__":
main()
```
```typescript
import "dotenv/config";
import fs from "fs";
import Anthropic from "@anthropic-ai/sdk";
import { Daytona, Sandbox } from "@daytona/sdk";
import type {
MessageParam,
Tool,
ToolUseBlock,
} from "@anthropic-ai/sdk/resources/messages.mjs";
interface CodeRunToolInput {
code: string;
}
interface ExecutionResult {
stdout: string;
exitCode: number;
charts?: Array<{ png?: string }>;
}
async function main() {
// Create sandbox
const daytona = new Daytona();
const sandbox = await daytona.create();
// Upload the dataset to the sandbox
await sandbox.fs.uploadFile("dataset.csv", "/home/daytona/dataset.csv");
const initialPrompt = `
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart.`;
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const tools: Tool[] = [
{
name: "run_python_code",
description: "Run Python code in the sandbox environment and get execution results",
input_schema: {
type: "object",
properties: {
code: {
type: "string",
description: "The Python code to run",
},
},
required: ["code"],
},
},
];
// Initialize conversation history
const messages: MessageParam[] = [
{ role: "user", content: initialPrompt },
];
let continueLoop = true;
let iterationCount = 0;
const maxIterations = 10;
console.log("Starting agentic loop...\n");
while (continueLoop && iterationCount < maxIterations) {
iterationCount++;
console.log(`\n=== Iteration ${iterationCount} ===`);
console.log("Waiting for the model response...");
// Get response from Claude
const stream = anthropic.messages.stream({
model: "claude-sonnet-4-5",
max_tokens: 64000,
messages: messages,
tools: tools,
});
const message = await stream.finalMessage();
// Log Claude's text response
for (const contentBlock of message.content) {
if (contentBlock.type === "text") {
console.log("\nClaude's response:");
console.log(contentBlock.text);
}
}
// Check if Claude wants to use any tools
const toolUses = message.content.filter(
(block): block is ToolUseBlock => block.type === "tool_use"
);
if (toolUses.length === 0) {
// No more tool uses, Claude is done
console.log("\nTask completed - no more actions needed.");
continueLoop = false;
break;
}
// Add Claude's response to message history
messages.push({
role: "assistant",
content: message.content,
});
// Execute all tool calls and collect results
const toolResults = [];
for (const toolUse of toolUses) {
if (toolUse.name === "run_python_code") {
const code = (toolUse.input as CodeRunToolInput).code;
console.log("\n--- Executing Python code in sandbox ---");
console.log(code);
console.log("--- End of code ---\n");
// Execute the code in the sandbox
const executionResult = await runAIGeneratedCode(sandbox, code);
// Format the tool result
let resultContent = "";
if (executionResult.exitCode === 0) {
resultContent += `Execution successful!\n\n`;
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`;
}
if (executionResult.charts && executionResult.charts.length > 0) {
resultContent += `\nGenerated ${executionResult.charts.length} chart(s).`;
} else {
resultContent += `\nNote: No charts were generated. Make sure to use plt.show() to display the chart.`;
}
} else {
resultContent += `Execution failed with exit code ${executionResult.exitCode}\n\n`;
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`;
}
}
toolResults.push({
type: "tool_result" as const,
tool_use_id: toolUse.id,
content: resultContent,
});
console.log("Execution result sent back to Claude.");
}
}
// Add tool results to conversation history
messages.push({
role: "user",
content: toolResults,
});
}
if (iterationCount >= maxIterations) {
console.log(
"\n⚠️ Reached maximum iteration limit. Task may not be complete."
);
}
console.log("\n=== Agentic loop completed ===");
}
async function runAIGeneratedCode(
sandbox: Sandbox,
aiGeneratedCode: string
): Promise {
const execution = await sandbox.process.codeRun(aiGeneratedCode);
const result: ExecutionResult = {
stdout: execution.result || "",
exitCode: execution.exitCode,
charts: execution.artifacts?.charts,
};
// Save any charts that were generated
if (execution.artifacts?.charts) {
let resultIdx = 0;
for (const chart of execution.artifacts.charts) {
if (chart.png) {
const filename = `chart-${resultIdx}.png`;
fs.writeFileSync(filename, chart.png, {
encoding: "base64",
});
console.log(`✓ Chart saved to ${filename}`);
resultIdx++;
}
}
}
return result;
}
main().catch(console.error);
```
# Build Coding Agent Using AgentKit and Daytona
import { Image } from 'astro:assets'
import notesAppResult from '../../../../../assets/docs/images/inngest-agentkit-notes-app.gif'
This guide demonstrates how to set up and use a fully autonomous coding agent that performs software development tasks in a [Daytona](https://daytona.io) sandbox environment. The agent is built using [AgentKit](https://agentkit.inngest.com/) and leverages Daytona sandboxes for secure, isolated execution. It can create web apps, run tests, execute scripts, and more; automating multi-step workflows based on user prompts.
---
### 1. Workflow Overview
You provide a natural language prompt describing the software task. The agent reasons about your request, plans the steps, and executes them securely in a Daytona sandbox; handling everything from project setup to live previews.
### 2. Project Setup
#### Clone the Repository
Clone the [repository](https://github.com/daytonaio/daytona) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/agentkit-inngest/coding-agent/anthropic
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run the coding agent locally. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
#### Docker
Build the Docker image:
```bash
docker buildx build . -t coding-agent
```
Run the container:
```bash
docker run --rm -it coding-agent
```
### 3. Configuration
- **Prompt Setting:** Edit the main prompt in `network.run(...)` in `src/index.ts` to change the agent's task.
- **Debug Logs:** Set `enableDebugLogs=true` for detailed agent flow tracking.
### 4. Example Usage
Try the default prompt in `src/index.ts`:
```typescript
const result = await network.run(
`Create a minimal React app called "Notes" that lets users add, view, and delete notes. Each note should have a title and content. Use Create React App or Vite for setup. Include a simple UI with a form to add notes and a list to display them.`
)
```
**The agent will:**
- Scaffold the app
- Install dependencies
- Start the dev server
- Generate a preview link
Terminal output example:
```
✔️ App is ready!
Preview: https://5173-3a828150-1573-42e3-bf9f-9793a2c2c0c2.proxy.daytona.works
```
The image below shows the result generated in this run:
### 5. Understanding the Agent's Execution Flow
The agent operates by leveraging a large language model (LLM) under the hood and has access to a suite of specialized tools. These tools enable the agent to perform everything needed inside the Daytona sandbox environment; from project setup to app deployment. At each step, the agent reasons about the current goal, decides which tool to call, and orchestrates the workflow to achieve the desired outcome.
Below is a step-by-step breakdown of how the agent builds the demo Notes app showing each iteration, the agent's message, and the tool used:
#### Project Initialization
**Agent message:**
```
I'll create a React app for Notes using Vite, which provides a faster and more modern setup compared to Create React App. I'll break this down into steps:
- First, I'll create the project using Vite
- Install necessary dependencies
- Create the components for the Notes app
- Implement the core functionality
Let's get started:
```
**Tool used:**
`shellTool` was called with command:
```
npm create vite@latest notes -- --template react
```
#### Install Dependencies
**Agent message:**
```
Now, I'll navigate to the project directory and install dependencies:
```
**Tool used:**
`shellTool` was called with command:
```
cd notes && npm install
```
#### Create Components & Update App Structure
**Agent message:**
```
Now, I'll create the necessary components and update the app structure:
1. Update App.jsx
2. Update App.css for basic styling
```
**Tool used:**
`uploadFilesTool` was called to upload:
```
App.jsx (main component logic)
App.css (basic styling)
```
#### Start Development Server
**Agent message:**
```
Now, I'll start the development server:
```
**Tool used:**
`startDevServerTool` was called with command:
```
cd notes && npm run dev
```
#### Check Dev Server Health
**Agent message:**
```
Let's check the dev server health:
```
**Tool used:**
`checkDevServerHealthTool` was called to verify that the development server is running and accessible.
#### App Summary & Features
**Agent message:**
```
Great! I've created a minimal React Notes app with the following features:
- Add notes with a title and content
- View the list of notes
- Delete individual notes
- Basic responsive styling
Key details:
- Used Vite for project setup
- Implemented state management with React hooks (useState)
- Simple form for adding notes
- Notes are stored in local state
- Each note has a unique ID, title, and content
- Delete functionality for individual notes
DEV_SERVER_PORT=5173
TASK_COMPLETED
The app is now running and ready to use. Users can:
1. Enter a note title in the first input
2. Enter note content in the textarea
3. Click "Add Note" to save the note
4. View notes in the list below
5. Delete notes by clicking the "Delete" button next to each note
Would you like me to explain any part of the implementation or make any modifications?
```
Notice `DEV_SERVER_PORT` and `TASK_COMPLETED` fields in the agent output message.
`DEV_SERVER_PORT` value is auto-detected by the agent and used to generate the preview link for your app.
`TASK_COMPLETED` signals that the task is finished which is used inside the agent routing logic.
---
**Key advantages:**
- Secure, isolated execution
- Multi-language support
- Auto-detects dev server, starts it, and generates preview link
- Detailed debug logs for agent actions
# OpenCode Guides
Guides for running [OpenCode](https://opencode.ai/) with Daytona.
# Run OpenCode with the Daytona Plugin
import { Image } from 'astro:assets'
import opencodePluginGif from '../../../../../assets/docs/images/opencode-plugin.gif'
This guide demonstrates how to run the [Daytona OpenCode plugin](https://www.npmjs.com/package/@daytona/opencode) which integrates Daytona sandboxes and OpenCode. When the plugin is active, all agent operations occur in secure sandboxes, with one sandbox per OpenCode session. The plugin also has the ability to sync changes between sandboxes and local Git branches.
### 1. Workflow Overview
When you run OpenCode with the Daytona plugin, sandboxes are created automatically inside OpenCode sessions. Operations such as running code, installing dependencies, and starting servers occur in the sandbox.
Sandboxes are preserved until you delete the OpenCode session. If a local Git repository is detected, the plugin syncs changes between the sandbox and branches with the `opencode/` prefix.
### 2. Project Setup
#### Add the Plugin
Add the Daytona plugin to your project by creating or editing `opencode.json` in the project directory:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@daytona/opencode"]
}
```
OpenCode downloads the plugin automatically when it starts. To install the plugin globally instead, edit `~/.config/opencode/opencode.json` and add the same `plugin` entry.
#### Configure Environment
This plugin requires a [Daytona account](https://www.daytona.io/) and [Daytona API key](https://app.daytona.io/dashboard/keys) to create sandboxes.
Set your API key:
```bash
export DAYTONA_API_KEY="your-api-key"
```
Or create a `.env` file in your project root:
```env
DAYTONA_API_KEY=your-api-key
```
#### Run OpenCode
:::note[Git required]
Ensure your project is a Git repository to enable syncing changes between the sandbox and your machine.
:::
Initialize Git if needed, then start OpenCode:
```bash
git init
opencode
```
You can now use OpenCode as usual. As you work, you will see notifications in OpenCode indicating sandboxes are being created and changes are being synced to local branches.
To confirm the plugin is working, type `pwd` in the chat and you should see a path like `/home/daytona/project`.
To view live logs from the plugin for debugging:
```bash
tail -f ~/.local/share/opencode/log/daytona.log
```
#### Version control
In your project directory, use Git to list and check out the branches OpenCode creates:
```bash
git branch
git checkout opencode/1
```
By default, new sessions start from the branch that was checked out when OpenCode was started. After this, synchronization only goes one way: from the sandbox to your local branch. To start working from a different branch, use git to check out that branch and start OpenCode again:
```bash
git checkout opencode/1
opencode
```
You can run as many OpenCode sessions in parallel as you want. Use Git to review and merge changes.
### 3. Understanding the Plugin Architecture
The Daytona plugin consists of several modules that provide custom tools, hooks for events, connections from sessions to sandboxes and system prompt transforms. These modules ensure every agent action runs in a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) and that changes sync to local Git branches:
- **Custom tools:** Overrides bash, read, write, edit, etc., so they run in the sandbox.
- **System prompt transform:** Injects sandbox path and instructions into the agent's system prompt.
- **Event handlers:** Handles session lifecycle events including cleanup (deleting sandboxes) and idle auto-commit (syncing changes to local git branches).
- **Session management:** Manages the creation and deletion of sandboxes and the mapping of sessions to sandboxes.
#### Custom tools
The custom tools module registers overrides for OpenCode's built-in tools so that every file and process operation goes through the Daytona SDK. It receives the project `id` and `worktree` from the plugin context and returns a tool map:
```typescript
export async function customTools(ctx: PluginInput, sessionManager: DaytonaSessionManager) {
logger.info('OpenCode started with Daytona plugin')
const projectId = ctx.project.id
const worktree = ctx.project.worktree
return {
bash: bashTool(sessionManager, projectId, worktree, ctx),
// ... read, write, edit, multiedit, patch, ls, glob, grep, lsp, getPreviewURL
}
}
```
For example, the plugin implementation of the bash tool uses the Daytona SDK to run the command in the sandbox:
```typescript
async execute(args: { command: string; background?: boolean }, ctx: ToolContext) {
const sessionId = ctx.sessionID
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree, pluginCtx)
if (args.background) {
// ... create or get exec session, then:
const result = await sandbox.process.executeSessionCommand(execSessionId, {
command: args.command,
runAsync: true,
})
return `Command started in background (cmdId: ${result.cmdId})`
} else {
const result = await sandbox.process.executeCommand(args.command, repoPath)
return `Exit code: ${result.exitCode}\n${result.result}`
}
}
```
All stateful tools (bash, read, write, edit, glob, grep, ls, lsp, multiedit, patch) are overridden the same way. The plugin also adds a custom tool for [preview links](https://www.daytona.io/docs/en/preview.md).
#### System prompt transform
The system prompt transform extends the system prompt to include instructions for the agent to work in the sandbox and use the background option for long-running commands:
```typescript
export async function systemPromptTransform(ctx: PluginInput, repoPath: string) {
return async (input: ExperimentalChatSystemTransformInput, output: ExperimentalChatSystemTransformOutput) => {
output.system.push(
[
'## Daytona Sandbox Integration',
'This session is integrated with a Daytona sandbox.',
`The main project repository is located at: ${repoPath}.`,
'Bash commands will run in this directory.',
'Put all projects in the project directory. Do NOT try to use the current working directory of the host system.',
"When executing long-running commands, use the 'background' option to run them asynchronously.",
'Before showing a preview URL, ensure the server is running in the sandbox on that port.',
].join('\n'),
)
}
}
```
#### Session events
The session events handler listens for OpenCode session lifecycle events and handles them appropriately. When you delete a session, the handler cleans up the corresponding sandbox. When a session becomes idle, it triggers auto-commit and sync:
```typescript
export async function eventHandlers(ctx: PluginInput, sessionManager: DaytonaSessionManager, repoPath: string) {
const projectId = ctx.project.id
const worktree = ctx.project.worktree
return async (args: any) => {
const event = args.event
if (event.type === EVENT_TYPE_SESSION_DELETED) {
const sessionId = (event as EventSessionDeleted).properties.info.id
await sessionManager.deleteSandbox(sessionId, projectId)
toast.show({ title: 'Session deleted', message: 'Sandbox deleted successfully.', variant: 'success' })
} else if (event.type === EVENT_TYPE_SESSION_IDLE) {
const sessionId = event.properties.sessionID
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree, ctx)
const branchNumber = sessionManager.getBranchNumberForSandbox(projectId, sandbox.id)
if (!branchNumber) return
const sessionGit = new SessionGitManager(sandbox, repoPath, worktree, branchNumber)
await sessionGit.autoCommitAndPull(ctx)
}
}
}
```
#### File synchronization
While OpenCode is in use, the plugin uses Git to keep session sandboxes and your local Git repository in sync. This only occurs if a git repository is detected in the project directory. On plugin start:
1. The plugin looks for a Git repository in the local directory.
2. A parallel repository is created in the sandbox with a single `opencode` branch, mirroring your current local branch.
3. A `sandbox` remote is added to your local repo using an SSH connection to the sandbox.
4. Your current `HEAD` is pushed to `opencode`, and the sandbox repo is reset to that state.
5. On session idle, the plugin commits in the sandbox on `opencode`, then pulls into a local branch (`opencode/1`, `opencode/2`, etc.) which is unique to each sandbox. A notification is shown when the sync is complete.
For more information on how the sync is implemented, see the [SessionGitManager](https://github.com/daytonaio/daytona/blob/main/libs/opencode-plugin/.opencode/plugin/daytona/git/session-git-manager.ts) class.
:::caution
When the plugin syncs to local `opencode` branches, any local changes on those branches are overwritten.
:::
#### Session storage
The session manager stores which sandbox belongs to each project in JSON files (using the same base path as OpenCode via `xdg-basedir`).
- **macOS:** `~/.local/share/opencode/storage/daytona/[projectid].json`
- **Windows:** `%LOCALAPPDATA%\opencode\storage\daytona\[projectid].json`
Each file holds sandbox metadata for that project's sessions so that sandboxes are retained between OpenCode uses.
**Key advantages:**
- Secure, isolated execution: each OpenCode session runs in its own Daytona sandbox
- Sandboxes persist until you delete the OpenCode session
- Live [preview links](https://www.daytona.io/docs/en/preview.md) when a server starts in the sandbox
- Automatic git sync to local branches so you can review and merge agent changes
- No script to run: add the plugin and use OpenCode as usual
# Build a Coding Agent Using OpenCode SDK and Daytona
import { Image } from 'astro:assets'
import opencodeSdkSyncboard from '../../../../../assets/docs/images/opencode-sdk-syncboard.gif'
This guide walks you through running the [OpenCode](https://opencode.ai/) autonomous coding agent inside a secure Daytona sandbox. The OpenCode server runs fully isolated in the sandbox and the script connects to the server using the [OpenCode SDK](https://www.npmjs.com/package/@opencode-ai/sdk).
The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate [preview links](https://www.daytona.io/docs/en/preview.md) for live apps.
### 1. Workflow Overview
When you run the script, it creates a Daytona sandbox, installs OpenCode, and starts the OpenCode server inside the sandbox. You send prompts and see responses in your terminal. On exit, the sandbox is deleted.
```
$ npm run start
Creating sandbox...
Installing OpenCode in sandbox...
Preview: https://4096-0626a08c-08e7-4fbe-9799-d160bac66b08.daytonaproxy01.net
Press Ctrl+C at any time to exit.
User: Create a minimalist collaborative whiteboard app
Thinking...
🔨 ✓ Run: Check current directory structure
✓ 3 todos
📝 Add home/daytona/SPEC.md
✓ 2 todos
📝 Add home/daytona/index.html
✓ 1 todos
✓ home/daytona/SPEC.md
✓ https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;700&display=swap (text/css; charset=utf-8)
✓ home/daytona/index.html
🔨 ✓ Run: List files in directory
🔨 ✓ Run: Start HTTP server for whiteboard
🔨 ✓ Run: Check if server is running
✓ 0 todos
Your minimalist collaborative whiteboard app is ready!
**Preview URL:** https://8080-0626a08c-08e7-4fbe-9799-d160bac66b08.daytonaproxy01.net
## SyncBoard - Features
**Drawing Tools:**
- Pencil (freehand drawing)
- Line, Rectangle, Ellipse (shape tools with preview)
- Eraser (removes intersecting strokes)
**Customization:**
- 6 vibrant color presets
- 3 stroke widths (thin/medium/thick)
- Keyboard shortcuts: `P` `L` `R` `E` `X` for tools, `1` `2` `3` for stroke widths
**Collaboration:**
- Simulated real-time collaboration with other users
- Unique user colors for each participant
- Auto-saves to localStorage every 2 seconds
**Actions:**
- Undo/Redo (stores 20 actions, Ctrl+Z / Ctrl+Shift+Z)
- Clear canvas (with confirmation dialog)
- Coordinates display in status bar
**Design:**
- Dark theme with coral/teal accents
- Dot grid background pattern
- Smooth animations and hover effects
- Responsive layout for mobile devices
User:
Cleaning up...
```
When your task involves running or previewing a web application, the agent can host the app and generate a [preview link](https://www.daytona.io/docs/en/preview.md) for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the Daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the OpenCode SDK example:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/opencode/opencode-sdk
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies and run the example:
```bash
npm install
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Script
OpenCode uses a client-server model: The [server](https://opencode.ai/docs/server/) manages coding agents, sessions and configuration. Clients communicate with the server [over HTTP](https://opencode.ai/docs/server/#apis) to run prompts in sessions and receive streamed responses.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs OpenCode globally in the sandbox.
3. Starts the OpenCode server in the sandbox.
4. Gets the preview URL for the server and creates a client with that base URL.
5. Enters the readline loop to send prompts to the server and receive streamed responses.
6. On Ctrl+C, deletes the sandbox and exits.
#### OpenCode Server
The OpenCode server runs inside the sandbox and handles all agent work: LLM calls, tools (bash, write, etc.), and code execution. The script starts it with:
```ts
const envVar = injectEnvVar('OPENCODE_CONFIG_CONTENT', opencodeConfig)
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${envVar} opencode serve --port ${PORT} --hostname ${HOSTNAME}`,
runAsync: true,
})
```
A custom configuration is injected which includes a system prompt. The system prompt tells the agent it runs in a sandbox, to use `/home/daytona` for file operations, and the preview URL pattern so it can provide correct preview links.
#### OpenCode Client
The client runs on your machine and talks to the server using the [OpenCode SDK](https://opencode.ai/docs/sdk/). `Session.create` sets up the SDK client and subscribes to the event stream so the client can show tool activity (e.g. “✓ Run: …”, “📝 Add …”) as the agent works:
```ts
const client = createOpencodeClient({ baseUrl })
const sessionRes = await client.session.create({ body: { title: 'Daytona query' } })
const sessionId = sessionRes.data?.id
if (!sessionId) throw new Error('Failed to create OpenCode session:' + sessionRes.error)
const events = await client.event.subscribe()
```
To send a prompt, the client calls `session.prompt` and processes events from the existing stream to show tool activity:
```ts
const promptPromise = this.client.session.prompt({
path: { id: this.sessionId },
body: { parts: [{ type: 'text', text: query } satisfies TextPartInput] },
})
for await (const event of takeUntil(this.events.stream, promptPromise)) {
printEvent(this.sessionId, event)
}
```
#### Main loop
When the server is ready, the script creates a session and runs a readline loop:
```ts
const session = await Session.create(baseUrl)
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
while (true) {
const query = await new Promise((resolve) => rl.question('User: ', resolve))
if (!query.trim()) continue
await session.runQuery(query)
}
```
The readline loop waits for user input, sends it to the agent, and prints the response.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- OpenCode SDK client in your terminal; server runs in the sandbox
- Support for 75+ LLM providers
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Custom agent configuration for Daytona-specific workflows
# Build a Coding Agent Using OpenCode Web and Daytona
import { Image } from 'astro:assets'
import opencodeResult from '../../../../../assets/docs/images/opencode-web-agent.gif'
This guide demonstrates how to run the [OpenCode](https://opencode.ai/) coding agent inside a Daytona sandbox environment using OpenCode's easy-to-use [web interface](https://opencode.ai/docs/web/).
The agent can develop web apps, write code in any language, install dependencies, and run scripts. It supports over 75 different LLM providers and can start dev servers with preview links for live apps.
---
### 1. Workflow Overview
When you launch the main script, a Daytona sandbox is created and OpenCode is installed inside it. OpenCode is configured with a custom Daytona-aware agent.
The script provides a preview link to access the web interface, where you can create, configure and interact with agent sessions:
```
$ npm run start
Creating sandbox...
Installing OpenCode...
Starting OpenCode web server...
Press Ctrl+C to stop.
▄
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█░░█ █░░█ █▀▀▀ █░░█ █░░░ █░░█ █░░█ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
Web interface: https://3000-1e0f775c-c01b-40e7-8c64-062fd3dadd75.proxy.daytona.works/
```
The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the Daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/opencode
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the example:
```bash
npm run start
```
The OpenCode web interface will start and wait for you to open it in your browser.
### Models and API Providers
OpenCode works with [over 75 LLM providers](https://opencode.ai/docs/providers/), with a free provider selected by default. You can change the model or provider at any time using the menu below the prompt input in the web interface. If your chosen provider needs an API key, you’ll be prompted to enter it.
#### Persisting API Keys
To persist API keys between uses of the script, you can set them as environment variables when creating the Daytona sandbox.
For example, to use an Anthropic API key, modify the `daytona.create()` call in `src/index.ts` to include your desired API key:
```typescript
sandbox = await daytona.create({
envVars: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '',
},
})
```
### 3. Understanding the Script
This example consists of a Node.js script that installs, configures and runs OpenCode inside a Daytona sandbox.
#### Initialization
On initialization, the main script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs OpenCode globally inside the sandbox using npm with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates and uploads a [custom agent configuration](https://opencode.ai/docs/agents/) with Daytona-specific system prompt.
4. Starts the OpenCode web server inside the sandbox on port 3000.
5. Substitutes the URL in OpenCode's output with a [Daytona preview link](https://www.daytona.io/docs/en/preview.md) for the web interface.
#### Main Script Code
The script creates a session and executes OpenCode as an asynchronous command, which allows it to stream output while keeping the process alive:
```typescript
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${envVar} opencode web --port ${OPENCODE_PORT}`,
runAsync: true,
})
```
When OpenCode starts its web server, it prints a link to its web UI using a localhost address (e.g., `http://127.0.0.1:3000`). However, since the sandbox runs remotely, this localhost link is only accessible inside the sandbox itself. To solve this, the script parses OpenCode’s output, and replaces the URL with the corresponding Daytona preview link.
```typescript
const opencodePreviewLink = await sandbox.getPreviewLink(OPENCODE_PORT)
const replaceUrl = (text: string) =>
text.replace(
new RegExp(`http:\\/\\/127\\.0\\.0\\.1:${OPENCODE_PORT}`, 'g'),
opencodePreviewLink.url
)
```
#### OpenCode Agent Configuration
A custom system prompt is used to instruct the agent on how to use Daytona sandbox paths and preview links. This prompt is packaged into a JSON configuration string, which is passed to the sandbox as the `OPENCODE_CONFIG_CONTENT` environment variable:
```json
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "daytona",
"agent": {
"daytona": {
"description": "Daytona sandbox-aware coding agent",
"mode": "primary",
"prompt": "You are running in a Daytona sandbox. Use the /home/daytona directory instead of /workspace for file operations. When running services on localhost, they will be accessible as: . When starting a server, always give the user the preview URL to access it. When starting a server, start it in the background with & so the command does not block further instructions."
}
}
}
```
The `` in the agent prompt is a template URL where `{PORT}` is a placeholder for the port to access on the Daytona sandbox. This template string is created by generating a [preview link](https://www.daytona.io/docs/en/preview.md) for a specific port number and then replacing the port number with `{PORT}`.
#### Clean up
When you press `Ctrl+C`, the script automatically cleans up by deleting the sandbox:
```typescript
process.once('SIGINT', async () => {
console.log('\nCleaning up...')
if (sandbox) await sandbox.delete()
process.exit(0)
})
```
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- OpenCode Web interface accessible via browser
- Support for 75+ LLM providers
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Custom agent configuration for Daytona-specific workflows
- Clean resource management with automatic sandbox cleanup
# Analyze Data With LangChain AI Agent
import { Image } from 'astro:assets'
import chartImage from '../../../../../assets/docs/images/langchain-data-analysis-chart.png'
This package provides the `DaytonaDataAnalysisTool` - LangChain tool integration that enables agents to perform secure Python data analysis in a sandboxed environment. It supports multi-step workflows, file uploads/downloads, and custom result handling, making it ideal for automating data analysis tasks with LangChain agents.
This page demonstrates the use of this tool with a basic example analyzing a vehicle valuations dataset. Our goal is to analyze how vehicle prices vary by manufacturing year and create a line chart showing average price per year.
---
### 1. Workflow Overview
You upload your dataset and provide a natural language prompt describing the analysis you want. The agent reasons about your request, determines how to use the `DaytonaDataAnalysisTool` to perform the task on your dataset, and executes the analysis securely in a Daytona sandbox.
You provide the data and describe what insights you need - the agent handles the rest.
### 2. Project Setup
#### Clone the Repository
Clone the [repository](https://github.com/daytonaio/daytona) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/python/langchain/data-analysis/anthropic
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher** because it uses LangChain 1.0+ syntax. It's recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install -U langchain langchain-anthropic langchain-daytona-data-analysis python-dotenv
```
The packages include:
- `langchain`: LangChain framework for building AI agents
- `langchain-anthropic`: Integration package connecting Claude (Anthropic) APIs and LangChain
- `langchain-daytona-data-analysis`: Provides the `DaytonaDataAnalysisTool` for LangChain agents
- `python-dotenv`: Used for loading environment variables from `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 3. Download Dataset
We'll be using a publicly available dataset of vehicle valuation. You can download it directly from:
[https://download.daytona.io/dataset.csv](https://download.daytona.io/dataset.csv)
Download the file and save it as `dataset.csv` in your project directory.
### 4. Initialize the Language Model
Models are the reasoning engine of LangChain agents - they drive decision-making, determine which tools to call, and interpret results.
In this example, we'll use Anthropic's Claude model, which excels at code generation and analytical tasks.
Configure the Claude model with the following parameters:
```python
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929",
temperature=0,
timeout=None,
max_retries=2,
stop=None
)
```
**Parameters explained:**
- `model_name`: Specifies the Claude model to use
- `temperature`: Tunes the degree of randomness in generation
- `max_retries`: Number of retries allowed for Anthropic API requests
:::tip[Learn More About Models]
For detailed information about LangChain models, different providers, and how to choose the right model for your use case, visit the [LangChain Models documentation](https://docs.langchain.com/oss/python/langchain/models).
:::
### 5. Define the Result Handler
When the agent executes Python code in the sandbox, it generates artifacts like charts and output logs. We can define a handler function to process these results.
This function will extract chart data from the execution artifacts and save them as PNG files:
```python
import base64
from daytona import ExecutionArtifacts
def process_data_analysis_result(result: ExecutionArtifacts):
# Print the standard output from code execution
print("Result stdout", result.stdout)
result_idx = 0
for chart in result.charts:
if chart.png:
# Charts are returned in base64 format
# Decode and save them as PNG files
with open(f'chart-{result_idx}.png', 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'Chart saved to chart-{result_idx}.png')
result_idx += 1
```
This handler processes execution artifacts by:
- Logging stdout output from the executed code
- Extracting chart data from the artifacts
- Decoding base64-encoded PNG charts
- Saving them to local files
### 6. Configure the Data Analysis Tool
Now we'll initialize the `DaytonaDataAnalysisTool` and upload our dataset.
```python
from langchain_daytona_data_analysis import DaytonaDataAnalysisTool
# Initialize the tool with our result handler
DataAnalysisTool = DaytonaDataAnalysisTool(
on_result=process_data_analysis_result
)
# Upload the dataset with metadata describing its structure
with open("./dataset.csv", "rb") as f:
DataAnalysisTool.upload_file(
f,
description=(
"This is a CSV file containing vehicle valuations. "
"Relevant columns:\n"
"- 'year': integer, the manufacturing year of the vehicle\n"
"- 'price_in_euro': float, the listed price of the vehicle in Euros\n"
"Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier."
)
)
```
**Key points:**
- The `on_result` parameter connects our custom result handler
- The `description` provides context about the dataset structure to the agent
- Column descriptions help the agent understand how to process the data
- Data cleaning instructions ensure quality analysis
### 7. Create and Run the Agent
Finally, we'll create the LangChain agent with our configured model and tool, then invoke it with our analysis request.
```python
from langchain.agents import create_agent
# Create the agent with the model and data analysis tool
agent = create_agent(model, tools=[DataAnalysisTool], debug=True)
# Invoke the agent with our analysis request
agent_response = agent.invoke({
"messages": [{
"role": "user",
"content": "Analyze how vehicles price varies by manufacturing year. Create a line chart showing average price per year."
}]
})
# Always close the tool to clean up sandbox resources
DataAnalysisTool.close()
```
**What happens here:**
1. The agent receives your natural language request
2. It determines it needs to use the `DaytonaDataAnalysisTool`
3. Agent generates Python code to analyze the data
4. Code executes securely in the Daytona sandbox
5. Results are processed by our handler function
6. Charts are saved to your local directory
7. Sandbox resources are cleaned up at the end
### 8. Running Your Analysis
Now you can run the complete code to see the results.
```bash
python data_analysis.py
```
#### Understanding the Agent's Execution Flow
When you run the code, the agent works through your request step by step. Here's what happens in the background:
**Step 1: Agent receives and interprets the request**
The agent acknowledges your analysis request:
```
AI Message: "I'll analyze how vehicle prices vary by manufacturing year and create a line chart showing the average price per year."
```
**Step 2: Agent generates Python code**
The agent generates Python code to explore the dataset first:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
# Display basic info about the dataset
print("Dataset shape:", df.shape)
print("\nFirst few rows:")
print(df.head())
print("\nColumn names:")
print(df.columns.tolist())
print("\nData types:")
print(df.dtypes)
```
**Step 3: Code executes in Daytona sandbox**
The tool runs this code in a secure sandbox and returns the output:
```
Result stdout Dataset shape: (100000, 15)
First few rows:
Unnamed: 0 ... offer_description
0 75721 ... ST-Line Hybrid Adapt.LED+Head-Up-Display Klima
1 80184 ... blue Trend,Viele Extras,Top-Zustand
2 19864 ... 35 e-tron S line/Matrix/Pano/ACC/SONOS/LM 21
3 76699 ... 2.0 Lifestyle Plus Automatik Navi FAP
4 92991 ... 1.6 T 48V 2WD Spirit LED, WR
[5 rows x 15 columns]
Column names:
['Unnamed: 0', 'brand', 'model', 'color', 'registration_date', 'year',
'price_in_euro', 'power_kw', 'power_ps', 'transmission_type', 'fuel_type',
'fuel_consumption_l_100km', 'fuel_consumption_g_km', 'mileage_in_km',
'offer_description']
Data types:
Unnamed: 0 int64
brand object
model object
color object
registration_date object
year object
price_in_euro object
power_kw object
power_ps object
transmission_type object
fuel_type object
fuel_consumption_l_100km object
fuel_consumption_g_km object
mileage_in_km float64
offer_description object
dtype: object
```
**Step 4: Agent generates detailed analysis code**
Based on the initial dataset information, the agent generates more specific code to examine the key columns:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
print("Dataset shape:", df.shape)
print("\nColumn names:")
print(df.columns.tolist())
# Check for year and price_in_euro columns
print("\nChecking 'year' column:")
print(df['year'].describe())
print("\nMissing values in 'year':", df['year'].isna().sum())
print("\nChecking 'price_in_euro' column:")
print(df['price_in_euro'].describe())
print("\nMissing values in 'price_in_euro':", df['price_in_euro'].isna().sum())
```
**Step 5: Execution results from sandbox**
The code executes and returns column statistics:
```
Result stdout Dataset shape: (100000, 15)
Column names:
['Unnamed: 0', 'brand', 'model', 'color', 'registration_date', 'year',
'price_in_euro', 'power_kw', 'power_ps', 'transmission_type', 'fuel_type',
'fuel_consumption_l_100km', 'fuel_consumption_g_km', 'mileage_in_km',
'offer_description']
Checking 'year' column:
count 100000
unique 49
top 2019
freq 12056
Name: year, dtype: object
Missing values in 'year': 0
Checking 'price_in_euro' column:
count 100000
unique 11652
top 19990
freq 665
Name: price_in_euro, dtype: object
Missing values in 'price_in_euro': 0
```
**Step 6: Agent generates final analysis and visualization code**
Now that the agent understands the data structure, it generates the complete analysis code with data cleaning, processing, and visualization:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
print("Original dataset shape:", df.shape)
# Clean the data - remove rows with missing values in year or price_in_euro
df_clean = df.dropna(subset=['year', 'price_in_euro'])
print(f"After removing missing values: {df_clean.shape}")
# Convert to numeric and remove non-numeric values
df_clean['year'] = pd.to_numeric(df_clean['year'], errors='coerce')
df_clean['price_in_euro'] = pd.to_numeric(df_clean['price_in_euro'], errors='coerce')
# Remove rows where conversion failed
df_clean = df_clean.dropna(subset=['year', 'price_in_euro'])
print(f"After removing non-numeric values: {df_clean.shape}")
# Remove outliers using IQR method for both year and price
def remove_outliers(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return df[(df[column] >= lower_bound) & (df[column] <= upper_bound)]
df_clean = remove_outliers(df_clean, 'year')
print(f"After removing year outliers: {df_clean.shape}")
df_clean = remove_outliers(df_clean, 'price_in_euro')
print(f"After removing price outliers: {df_clean.shape}")
print("\nCleaned data summary:")
print(df_clean[['year', 'price_in_euro']].describe())
# Calculate average price per year
avg_price_by_year = df_clean.groupby('year')['price_in_euro'].mean().sort_index()
print("\nAverage price by year:")
print(avg_price_by_year)
# Create line chart
plt.figure(figsize=(14, 7))
plt.plot(avg_price_by_year.index, avg_price_by_year.values, marker='o',
linewidth=2, markersize=6, color='#2E86AB')
plt.xlabel('Manufacturing Year', fontsize=12, fontweight='bold')
plt.ylabel('Average Price (€)', fontsize=12, fontweight='bold')
plt.title('Average Vehicle Price by Manufacturing Year', fontsize=14,
fontweight='bold', pad=20)
plt.grid(True, alpha=0.3, linestyle='--')
plt.xticks(rotation=45)
# Format y-axis to show currency
ax = plt.gca()
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'€{x:,.0f}'))
plt.tight_layout()
plt.show()
# Additional statistics
print(f"\nTotal number of vehicles analyzed: {len(df_clean)}")
print(f"Year range: {int(df_clean['year'].min())} - {int(df_clean['year'].max())}")
print(f"Price range: €{df_clean['price_in_euro'].min():.2f} - €{df_clean['price_in_euro'].max():.2f}")
print(f"Overall average price: €{df_clean['price_in_euro'].mean():.2f}")
```
This comprehensive code performs data cleaning, outlier removal, calculates averages by year, and creates a professional visualization.
**Step 7: Final execution and chart generation**
The code executes successfully in the sandbox, processes the data, and generates the visualization:
```
Result stdout Original dataset shape: (100000, 15)
After removing missing values: (100000, 15)
After removing non-numeric values: (99946, 15)
After removing year outliers: (96598, 15)
After removing price outliers: (90095, 15)
Cleaned data summary:
year price_in_euro
count 90095.000000 90095.000000
mean 2016.698563 22422.266707
std 4.457647 12964.727116
min 2005.000000 150.000000
25% 2014.000000 12980.000000
50% 2018.000000 19900.000000
75% 2020.000000 29500.000000
max 2023.000000 62090.000000
Average price by year:
year
2005.0 5968.124319
2006.0 6870.881523
2007.0 8015.234473
2008.0 8788.644495
2009.0 8406.198576
2010.0 10378.815972
2011.0 11540.640435
2012.0 13306.642261
2013.0 14512.707025
2014.0 15997.682899
2015.0 18563.864358
2016.0 20124.556294
2017.0 22268.083322
2018.0 24241.123673
2019.0 26757.469111
2020.0 29400.163494
2021.0 30720.168646
2022.0 33861.717552
2023.0 33119.840175
Name: price_in_euro, dtype: float64
Total number of vehicles analyzed: 90095
Year range: 2005 - 2023
Price range: €150.00 - €62090.00
Overall average price: €22422.27
Chart saved to chart-0.png
```
The agent successfully completed the analysis, showing that vehicle prices generally increased from 2005 (€5,968) to 2022 (€33,862), with a slight decrease in 2023. The result handler captured the generated chart and saved it as `chart-0.png`.
You should see the chart in your project directory that will look similar to this:
### 9. Complete Implementation
Here is the complete, ready-to-run example:
```python
import base64
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from daytona import ExecutionArtifacts
from langchain_daytona_data_analysis import DaytonaDataAnalysisTool
load_dotenv()
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929",
temperature=0,
timeout=None,
max_retries=2,
stop=None
)
def process_data_analysis_result(result: ExecutionArtifacts):
# Print the standard output from code execution
print("Result stdout", result.stdout)
result_idx = 0
for chart in result.charts:
if chart.png:
# Save the png to a file
# The png is in base64 format.
with open(f'chart-{result_idx}.png', 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'Chart saved to chart-{result_idx}.png')
result_idx += 1
def main():
DataAnalysisTool = DaytonaDataAnalysisTool(
on_result=process_data_analysis_result
)
try:
with open("./dataset.csv", "rb") as f:
DataAnalysisTool.upload_file(
f,
description=(
"This is a CSV file containing vehicle valuations. "
"Relevant columns:\n"
"- 'year': integer, the manufacturing year of the vehicle\n"
"- 'price_in_euro': float, the listed price of the vehicle in Euros\n"
"Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier."
)
)
agent = create_agent(model, tools=[DataAnalysisTool], debug=True)
agent_response = agent.invoke(
{"messages": [{"role": "user", "content": "Analyze how vehicles price varies by manufacturing year. Create a line chart showing average price per year."}]}
)
finally:
DataAnalysisTool.close()
if __name__ == "__main__":
main()
```
**Key advantages of this approach:**
- **Secure execution:** Code runs in isolated Daytona sandbox
- **Automatic artifact capture:** Charts, tables, and outputs are automatically extracted
- **Natural language interface:** Describe analysis tasks in plain English
- **Framework integration:** Seamlessly works with LangChain's agent ecosystem
### 10. API Reference
The following public methods are available on `DaytonaDataAnalysisTool`:
#### download_file
```python
def download_file(remote_path: str) -> bytes
```
Downloads a file from the sandbox by its remote path.
**Arguments**:
- `remote_path` - str: Path to the file in the sandbox.
**Returns**:
- `bytes` - File contents.
**Example**:
```python
# Download a file from the sandbox
file_bytes = tool.download_file("/home/daytona/results.csv")
```
#### upload_file
```python
def upload_file(file: IO, description: str) -> SandboxUploadedFile
```
Uploads a file to the sandbox. The file is placed in `/home/daytona/`.
**Arguments**:
- `file` - IO: File-like object to upload.
- `description` - str: Description of the file, explaining its purpose and the type of data it contains.
**Returns**:
- [`SandboxUploadedFile`](#sandboxuploadedfile) - Metadata about the uploaded file.
**Example**:
Suppose you want to analyze sales data for a retail business. You have a CSV file named `sales_q3_2025.csv` containing columns like `transaction_id`, `date`, `product`, `quantity`, and `revenue`. You want to upload this file and provide a description that gives context for the analysis.
```python
with open("sales_q3_2025.csv", "rb") as f:
uploaded = tool.upload_file(
f,
"CSV file containing Q3 2025 retail sales transactions. Columns: transaction_id, date, product, quantity, revenue."
)
```
#### remove_uploaded_file
```python
def remove_uploaded_file(uploaded_file: SandboxUploadedFile) -> None
```
Removes a previously uploaded file from the sandbox.
**Arguments**:
- `uploaded_file` - [`SandboxUploadedFile`](#sandboxuploadedfile): The file to remove.
**Returns**:
- None
**Example**:
```python
# Remove an uploaded file
tool.remove_uploaded_file(uploaded)
```
#### get_sandbox
```python
def get_sandbox() -> Sandbox
```
Gets the current sandbox instance.
This method provides access to the Daytona sandbox instance, allowing you to inspect sandbox properties and metadata, as well as perform any sandbox-related operations. For details on available attributes and methods, see the [Sandbox](#sandbox) data structure section below.
**Arguments**:
- None
**Returns**:
- [`Sandbox`](#sandbox) - Sandbox instance.
**Example**:
```python
sandbox = tool.get_sandbox()
```
#### install_python_packages
```python
def install_python_packages(package_names: str | list[str]) -> None
```
Installs one or more Python packages in the sandbox using pip.
**Arguments**:
- `package_names` - str | list[str]: Name(s) of the package(s) to install.
**Returns**:
- None
:::note
The list of preinstalled packages in a sandbox can be found at [Daytona's Default Snapshot documentation](https://www.daytona.io/docs/en/snapshots.md#default-snapshots).
:::
**Example**:
```python
# Install a single package
tool.install_python_packages("pandas")
# Install multiple packages
tool.install_python_packages(["numpy", "matplotlib"])
```
#### close
```python
def close() -> None
```
Closes and deletes the sandbox environment.
**Arguments**:
- None
**Returns**:
- None
:::note
Call this method when you are finished with all data analysis tasks to properly clean up resources and avoid unnecessary usage.
:::
**Example**:
```python
# Close the sandbox and clean up
tool.close()
```
### 11. Data Structures
#### SandboxUploadedFile
Represents metadata about a file uploaded to the sandbox.
- `name`: `str` - Name of the uploaded file in the sandbox
- `remote_path`: `str` - Full path to the file in the sandbox
- `description`: `str` - Description provided during upload
#### Sandbox
Represents a Daytona sandbox instance.
See the full structure and API in the [Daytona Python SDK Sandbox documentation](https://www.daytona.io/docs/en/python-sdk/sync/sandbox.md#sandbox).
# Build a Plan-and-Execute Data Agent With LangGraph and Daytona
import { Image } from 'astro:assets'
import workflowDiagram from '../../../../../assets/docs/images/langgraph-plan-and-execute-workflow.svg'
This guide demonstrates how to build a [LangGraph](https://langchain-ai.github.io/langgraph/) **plan-and-execute** data agent that runs an end-to-end ETL plus analytical-SQL workflow inside a [Daytona](https://daytona.io) sandbox. The graph is hand-wired as a six-node state machine. Every node and every edge is explicit, so the agent's control flow is fully inspectable.
In this example, we ask the agent to profile the maintenance health of the public [`langchain-ai/langgraph`](https://github.com/langchain-ai/langgraph) GitHub repository: extract issues and pull requests from the public GitHub REST API, transform and normalize them into a relational schema, load them into a SQLite database in the sandbox, run three analytical SQL queries, and report findings.
---
### 1. Workflow Overview
You ask the agent a natural-language data question. The planner emits an ordered list of atomic plan steps as structured output. For each step the executor generates Python code and runs it in a Daytona sandbox. A deterministic `check` node advances to the next step on success, retries the current step (with the failing code as context, up to `max_attempts`) on failure, or routes to the summarizer once the plan is complete. The summarizer produces a final natural-language answer and a `cleanup` node deletes the sandbox.
The key benefit: every node, edge, and retry decision lives in plain Python you can read, not inside a prebuilt agent loop.
### 2. Project Setup
#### Clone the Repository
Clone the [Daytona repository](https://github.com/daytonaio/daytona) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/python/langgraph/plan-and-execute-data-agent
```
#### Create Virtual Environment
:::note[Python Version]
Python 3.10 or higher is required.
:::
```bash
python3.10 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
#### Install Dependencies
```bash
pip install -U langgraph langchain-core langchain-anthropic daytona pydantic python-dotenv
```
The packages include:
- `langgraph`: Graph-structured orchestration (provides `StateGraph` and conditional-edge routing)
- `langchain-core`: Message types (`HumanMessage`, `SystemMessage`, `BaseMessage`)
- `langchain-anthropic`: `ChatAnthropic` chat model, including the Anthropic-specific `with_structured_output` implementation we use for the planner
- `daytona`: Daytona Python SDK for sandbox provisioning and code execution
- `pydantic`: Defines the structured plan schema returned by the planner LLM call
- `python-dotenv`: Loads environment variables from `.env`
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 3. Understanding the Core Components
Before walking through the implementation, here are the key concepts the code relies on: the choice of agent pattern (plan-and-execute vs. ReAct), LangGraph's `StateGraph` with task-specific state, how state updates flow through nodes, structured-output planning, and the Daytona code interpreter.
#### Plan-and-Execute vs. ReAct
LangGraph supports many agent topologies; plan-and-execute is one common pattern, and ReAct is its most-cited counterpoint. Understanding what plan-and-execute changes is easiest if you first know how ReAct works.
ReAct (short for **Reasoning + Acting**) is one of the most common patterns for structuring an agent loop. In ReAct the model alternates between two kinds of output: **reasoning** (free-form thinking about what to do next, sometimes visible as text, sometimes implicit in modern function-calling implementations) and **action** (a tool call). After each action, the tool's result (the **observation**) is appended to the conversation, and the model is invoked again. The loop continues until the model emits a final natural-language answer instead of another tool call. There is no upfront plan as a separate data structure; the agent's strategy emerges incrementally and is recorded only as the running chain of messages, tool calls, and observations in the conversation history.
Plan-and-execute splits these stages apart. The planner emits a complete plan **as data** (an ordered `list[str]` of atomic steps) before any code runs, and that plan lives in graph state where you can read it, log it, modify it, or replay individual steps. A separate `execute` node then implements each plan step in sequence, while a deterministic `check` node tracks per-step retry attempts and routes between `execute` and `summarize`. The failure-recovery loop is explicit in state rather than buried inside an agent's internal reasoning, which makes the whole control flow auditable.
This guide implements plan-and-execute because the demo task (ETL into SQLite plus three analytical queries) has a roughly fixed shape the LLM can plan in advance.
#### `StateGraph` with custom state
State is a `TypedDict` with task-specific fields rather than just `messages`. This is the canonical LangGraph pattern for non-chat workflows: state carries everything the nodes need to communicate.
```python
class AgentState(TypedDict):
sandbox: Sandbox | None
user_request: str
plan: list[str]
step_idx: int
attempts: int
max_attempts: int
last_error: str | None
last_code: str | None
step_outputs: list[str]
step_codes: list[str]
final_answer: str
```
`step_codes` and `step_outputs` are append-only lists indexed by completed plan step. The executor reads them on every call so each new step has full context about what variables and files prior steps produced.
#### How nodes update state
Nodes don't mutate `state` in place. Each node has the signature `(state: AgentState) -> dict`, and the dict it returns is **merged into the state** by the framework. The default merge is **replace-per-key**: any key present in the returned dict overwrites the previous value; keys absent from the return value stay unchanged. Returning `{}` means the node read state without changing it.
That replace-per-key default is why the `execute` node's success-path return constructs the new list explicitly:
```python
return {
"step_outputs": state["step_outputs"] + [stdout], # build new list with appended item
"step_codes": state["step_codes"] + [code],
...
}
```
If we instead returned `{"step_outputs": [stdout]}`, the framework would _replace_ the whole list with one element, losing prior outputs.
LangGraph also supports **reducers** as an alternative: annotating a state field with one (for example, `Annotated[list[str], operator.add]`) tells the framework to concatenate instead of replace, so a return of `{"step_outputs": [stdout]}` would auto-append. This guide deliberately doesn't use reducers, so the merge logic lives where the data is constructed rather than hiding in the schema annotation.
One last detail worth knowing: when a conditional edge fires after a node, the routing function (e.g., `route_from_check`) sees the **post-merge** state. The node's returned dict is applied to state _before_ the router runs, so the router always reads the updated values.
#### Structured-output planning
The `plan` node uses `model.with_structured_output(Plan)` where `Plan` is a Pydantic model with one field, `steps: list[str]`. The Anthropic adapter converts this into a tool-style schema and forces the LLM to return a parseable list rather than free-form prose. This is more reliable than parsing markdown-bulleted lists by regex.
#### Daytona SDK
`Daytona().create()` provisions a sandbox. The LLM-generated code from each `execute` step is run by `sandbox.code_interpreter.run_code(code)`. We use the `code_interpreter` API specifically because it preserves the Python execution context across calls: imports, variables, and functions defined in one plan step are still in scope in the next.
### 4. Implementation
#### Step 1: State, schema, and prompts
```python
from typing import TypedDict
from pydantic import BaseModel, Field
from daytona import Sandbox
class Plan(BaseModel):
steps: list[str] = Field(description="Atomic plan steps the executor will implement, in order.")
class AgentState(TypedDict):
sandbox: Sandbox | None
user_request: str
plan: list[str]
step_idx: int
attempts: int
max_attempts: int
last_error: str | None
last_code: str | None
step_outputs: list[str]
step_codes: list[str]
final_answer: str
```
The `description` argument on `Field(...)` in `Plan` is sent to the model as the schema's natural-language hint, which steers what the LLM puts in each list entry.
The fields of `AgentState` group into five roles based on which nodes write to them:
- **Lifecycle**: `sandbox` (set by `provision`, nulled by `cleanup`), `user_request` (seeded at startup, immutable for the run).
- **Plan progress**: `plan` (the list of steps from the planner) and `step_idx` (which step is currently being executed).
- **Retry tracking**: `attempts`, `max_attempts`, plus `last_error` and `last_code` (written by `execute` on failure, read by the next attempt so the LLM sees the error and failing code).
- **Append-only history**: `step_outputs` and `step_codes`, one entry per successfully completed step.
- **Output**: `final_answer`, populated by `summarize` once the run finishes.
Three system prompts shape the three LLM calls that the graph makes. They are defined at module level alongside the schemas so all the static configuration lives in one place.
**Planner prompt.** Drives the `plan` node. The critical rule is **preserve URLs and identifiers verbatim**: without it, the planner tends to paraphrase ("fetch from the GitHub API") and the executor then has to guess. The other rules constrain the number of steps and discourage bundling tightly-coupled work across step boundaries (which would force the executor to guess variable names from one step in the next).
```python
PLAN_SYSTEM_PROMPT = """You are the planner stage of a plan-and-execute data agent.
Produce an ordered list of 4-8 atomic plan steps. Each step is one natural-language sentence describing
a single chunk of Python code that the executor stage will then write and run in a persistent Daytona sandbox.
Rules:
- Sandbox state PERSISTS across steps. Imports, variables, and files from step N are visible in step N+1.
- Step 1 should establish any package installs or imports.
- Each step is one coherent action. Group tightly-coupled work that shares variables (fetch + filter, or
create-schema + insert-data) into a SINGLE step so the executor doesn't have to guess prior variable names
across step boundaries. Keep loosely-coupled work in separate steps.
- PRESERVE any specific URLs, endpoints, file paths, table names, or identifiers from the user's request
VERBATIM inside the plan step that uses them. Do not paraphrase URLs.
- Do NOT write code in the plan. Describe what each step does.
"""
```
**Executor prompt.** Drives the `execute` node. Forces the LLM to output **only** Python in a fenced code block (the wrapper regex `extract_code()` depends on this format), to use exact URLs/paths from the user's request, and to use the exact variable names assigned by prior steps' code (which the prompt template injects into the user message). On a retry, the prompt also asks the LLM to diagnose and produce a _materially different_ fix rather than retry the same approach.
````python
EXECUTE_SYSTEM_PROMPT = """You are the executor stage of a plan-and-execute data agent.
You receive the user's original request, the full plan, and the index of the current step. You must output
ONLY Python code that accomplishes the current step. The code runs in a persistent Daytona sandbox; prior
steps' variables, imports, and files are still in scope. Always `print()` the relevant output so later
stages can see the results.
Rules:
- Use the EXACT URLs / endpoints / file paths from the user's original request. Do not invent or paraphrase.
- CRITICAL: Before referencing any variable from a prior step, scan the prior code shown below and use
EXACTLY the variable name that the prior step assigned. Never invent variable names. If you cannot find
the variable you need in the prior code, re-derive it from scratch within your current step.
- Output format: a single ```python fenced block, nothing else. No prose.
- If a previous attempt failed, you will see the error and the failing code. Diagnose the root cause and
produce a materially different fix. Do not repeat the failing approach. If the error is a NameError,
the missing variable was never defined in the shown prior code; re-derive it from raw data.
"""
````
**Summarizer prompt.** Drives the `summarize` node. The whole job of this prompt is to keep the final answer **honest**: cite numbers that appear in the stdout, do not hallucinate values that don't appear there, and report failures plainly if the agent gave up. This is the safety belt that prevents the LLM from fabricating plausible-looking results when the actual run was incomplete.
```python
SUMMARIZE_SYSTEM_PROMPT = """You are the summarizer stage of a plan-and-execute data agent.
You will be shown the user's original request and the stdout from each successfully executed plan step.
Produce a clear, factual answer in 1-3 short paragraphs. Cite specific numbers from the stdout. Do not
hallucinate values that are not present in the stdout. If a step failed, say so plainly.
"""
```
#### Step 2: The `provision` and `plan` nodes
```python
plan_llm = model.with_structured_output(Plan)
def provision(state: AgentState) -> dict:
sandbox = Daytona().create()
return {"sandbox": sandbox}
def plan_node(state: AgentState) -> dict:
result = plan_llm.invoke([
SystemMessage(content=PLAN_SYSTEM_PROMPT),
HumanMessage(content=state["user_request"]),
])
return {"plan": result.steps}
```
#### Step 3: The `execute` node
This is the node that does the real work of the agent. On every call, the executor receives the **complete context of the run so far**, assembled from graph state:
- the **original user request** (so URLs, paths, and identifiers are never lost through paraphrasing)
- the **full plan** with the current step marked, so the LLM sees what came before and what comes after
- every **prior step's generated code** (variables, imports, helper functions that prior code defined are still in scope in the sandbox, but the LLM also needs to see them to _reuse_ names correctly)
- every **prior step's stdout** (what those steps actually printed)
- and, on retry, the **previous attempt's error and failing code**
That last bullet is the key recovery mechanism. When the executor LLM has the failing code in front of it together with the traceback, it can diagnose the problem and produce a materially different fix rather than blindly retrying.
```python
def execute(state: AgentState) -> dict:
idx = state["step_idx"]
step_text = state["plan"][idx]
plan_listing = "\n".join(
f" {i + 1}. {s}{' <-- CURRENT' if i == idx else ''}" for i, s in enumerate(state["plan"])
)
prompt_parts = [
f"Original user request:\n{state['user_request']}",
f"Full plan:\n{plan_listing}",
f"Current step ({idx + 1} of {len(state['plan'])}): {step_text}",
]
if state["step_codes"]:
prompt_parts.append("Code already executed in this sandbox (variables and imports still in scope):")
for i, prior in enumerate(state["step_codes"], 1):
prompt_parts.append(f"--- step {i} code ---\n{prior}")
if state["step_outputs"]:
prompt_parts.append("Stdout from those prior steps:")
for i, output in enumerate(state["step_outputs"], 1):
prompt_parts.append(f"--- step {i} stdout ---\n{output[:1500]}")
if state["last_error"] and state["last_code"]:
prompt_parts.append(f"--- previous attempt error ---\n{state['last_error'][:1500]}")
prompt_parts.append(f"--- previous failing code ---\n{state['last_code']}")
prompt_parts.append("Diagnose and write a corrected implementation.")
response = model.invoke([
SystemMessage(content=EXECUTE_SYSTEM_PROMPT),
HumanMessage(content="\n\n".join(prompt_parts)),
])
content = response.content if isinstance(response.content, str) else str(response.content)
code = extract_code(content)
sandbox = state["sandbox"]
result = sandbox.code_interpreter.run_code(code, timeout=180)
stdout = result.stdout or ""
if result.error is not None:
err = f"{result.error.name}: {result.error.value}\n{result.error.traceback}".strip()
return {"last_error": err, "last_code": code}
return {
"last_error": None,
"last_code": code,
"step_outputs": state["step_outputs"] + [stdout],
"step_codes": state["step_codes"] + [code],
}
```
Note the `+ [stdout]` and `+ [code]` patterns in the success-path return. `step_outputs` and `step_codes` are both `list[str]` in the state schema; this idiom _appends_ to those lists by constructing a **new** list (`existing + [new_item]`) rather than mutating the original in place. Returning new values is the LangGraph convention because the framework reasons about state as a sequence of immutable snapshots, which enables checkpointing, time-travel debugging, and replay.
Two more details worth calling out:
- **Marking the current step** with `<-- CURRENT` in the plan listing nudges the LLM to focus on that step without losing sight of the surrounding ones. It can see what was already done and what remains.
- **Sandbox state vs. shown context** are two different things. The interpreter context literally still holds whatever prior steps imported or assigned, but the LLM has no introspection into that running state. Passing prior code as text gives the LLM the symbolic view it needs to reuse names correctly.
#### Step 4: The `check` node and conditional routing
`check` does **state mutation only**. It does not decide where the graph goes. The routing decision lives in `route_from_check`, a function passed to `add_conditional_edges`.
```python
def check(state: AgentState) -> dict:
if state["last_error"]:
return {"attempts": state["attempts"] + 1}
return {
"step_idx": state["step_idx"] + 1,
"attempts": 0,
"last_error": None,
"last_code": None,
}
def route_from_check(state: AgentState) -> str:
if state["last_error"]:
if state["attempts"] >= state["max_attempts"]:
return "summarize"
return "execute"
if state["step_idx"] >= len(state["plan"]):
return "summarize"
return "execute"
```
Splitting state mutation from routing keeps each function pure and the graph topology readable: `check` updates counters, `route_from_check` picks an edge.
The conditional edge itself is wired with `add_conditional_edges`:
```python
graph.add_conditional_edges("check", route_from_check, {"execute": "execute", "summarize": "summarize"})
```
This call takes three arguments: the **source node** (`"check"`), the **routing function** (`route_from_check`), and a **mapping dict** that translates the routing function's return value to a destination node name. At runtime LangGraph calls `route_from_check(state)`, gets back a string (here either `"execute"` or `"summarize"`), looks it up as a key in the mapping dict, and routes to the value (the actual node name).
In our case the dict's keys and values are identical because the routing function happens to return literal node names. That looks redundant, but the dict layer is the convention even when they match. The reason is decoupling: a routing function can return _semantic labels_ like `"high_priority"` or `"needs_retry"`, and the dict translates those labels to whatever the graph's actual node names are. This lets you reuse routing functions across graphs and rename nodes without touching the routing logic. You can also use the special `END` constant as a destination to terminate the graph from a conditional branch (for example, `{"continue": "next_node", END: END}`).
#### Step 5: `summarize` and `cleanup`
```python
def summarize(state: AgentState) -> dict:
parts = [f"Original request:\n{state['user_request']}", "Outputs from executed plan steps:"]
for i, output in enumerate(state["step_outputs"], 1):
parts.append(f"--- step {i} stdout ---\n{output}")
if state["last_error"]:
parts.append(f"NOTE: the agent gave up before finishing. Last error:\n{state['last_error']}")
response = model.invoke([
SystemMessage(content=SUMMARIZE_SYSTEM_PROMPT),
HumanMessage(content="\n\n".join(parts)),
])
content = response.content if isinstance(response.content, str) else str(response.content)
return {"final_answer": content}
def cleanup(state: AgentState) -> dict:
sandbox = state.get("sandbox")
if sandbox is not None:
sandbox.delete()
return {"sandbox": None}
```
#### Step 6: Wire the graph
```python
graph = StateGraph(AgentState)
graph.add_node("provision", provision)
graph.add_node("plan", plan_node)
graph.add_node("execute", execute)
graph.add_node("check", check)
graph.add_node("summarize", summarize)
graph.add_node("cleanup", cleanup)
graph.add_edge(START, "provision")
graph.add_edge("provision", "plan")
graph.add_edge("plan", "execute")
graph.add_edge("execute", "check")
graph.add_conditional_edges("check", route_from_check, {"execute": "execute", "summarize": "summarize"})
graph.add_edge("summarize", "cleanup")
graph.add_edge("cleanup", END)
app = graph.compile()
```
#### Step 7: The `main()` entry point
The `main()` function is the entry point that ties everything together: instantiate the chat model, build the compiled graph, seed the initial state, invoke the graph, and print the result.
```python
def main() -> None:
model = ChatAnthropic(
model_name="claude-opus-4-6",
temperature=0,
timeout=None,
max_retries=2,
stop=None,
)
app = build_graph(model)
initial_state: AgentState = {
"sandbox": None,
"user_request": USER_REQUEST,
"plan": [],
"step_idx": 0,
"attempts": 0,
"max_attempts": 3,
"last_error": None,
"last_code": None,
"step_outputs": [],
"step_codes": [],
"final_answer": "",
}
final_state = app.invoke(initial_state, config={"recursion_limit": 50})
print(final_state["final_answer"])
```
A few things worth calling out:
- **`initial_state`** is a `dict` with every `AgentState` field set to its starting value. The `provision` node writes the real `sandbox` object; the `plan` node populates `plan`; the `execute` node appends to `step_outputs` and `step_codes`; `check` mutates `step_idx` and `attempts`. Everything starts empty/zero and is filled in by the graph.
- **`config={"recursion_limit": 50}`** raises LangGraph's per-invocation **super-step budget** from its default of 25. A super-step is one node execution; LangGraph aborts the run with `GraphRecursionError` once that count is exceeded. For this guide the canonical plan has 6 steps (3 setup steps: install/import, fetch from GitHub, create + load SQLite; plus 3 analytical SQL queries), so a clean run uses about `1 (provision) + 1 (plan) + 6 × 2 (execute → check, once per plan step) + 1 (summarize) + 1 (cleanup) = 16` super-steps. Each retry adds another `execute → check` pair on top. The default of 25 is tight once retries fire; 50 leaves comfortable headroom.
- **`final_state["final_answer"]`** is the natural-language report produced by the `summarize` node. The rest of `final_state` still contains the full plan, all per-step code and outputs, the final `sandbox=None` (cleanup nulled it after deletion), etc. so you can inspect or persist any of it.
### 5. Run the Example
```bash
python main.py
```
The agent typically emits a 6-step plan, executes each step in the persistent interpreter context, runs three analytical SQL queries, and summarizes. Because variables and imports survive across steps, an `import requests` in step 1 is still in scope when step 2 calls `requests.get(...)`, so well-formed code rarely needs the retry path. The canonical run below completes all six steps on the first attempt.
#### Example Output
```text
[provision] creating Daytona sandbox...
[provision] sandbox ready (id=b9cf758d-9b93-4117-96b3-9a406c86b1b8)
[plan] asking the LLM for a multi-step plan...
[plan] 6 step(s):
1. Install needed packages and import requests, sqlite3, json, datetime
2. Fetch the 100 most recently updated issues and PRs from the
langchain-ai/langgraph /issues and /pulls endpoints; store the JSON responses
3. Create a SQLite database (langgraph.db) with two tables (issues, pull_requests),
define schemas, and insert the fetched data
4. SQL: PR merge rate among closed PRs
5. SQL: top 5 PR authors by count with personal merge rates
6. SQL: most-commented currently-open issue
[execute] step 1/6 attempt 1/3: Install needed packages...
[execute] step OK.
[check] step 1 done; advancing to step 2
[execute] step 2/6 attempt 1/3: Fetch the 100 most recently updated issues and PRs...
[execute] step OK. stdout:
Fetched 100 issues, 100 pull requests
[check] step 2 done; advancing to step 3
[execute] step 3/6 attempt 1/3: Create a SQLite database, define schemas, insert data...
[execute] step OK. stdout:
Created database with two tables: issues, pull_requests
Inserted 40 issues (after filtering out PRs from the /issues endpoint) and 100 PRs
[check] step 3 done; advancing to step 4
[execute] step 4/6 attempt 1/3: SQL: PR merge rate among closed PRs...
[execute] step OK. stdout:
SELECT COUNT(*), SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END),
ROUND(100.0 * SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*), 2)
FROM pull_requests WHERE state = 'closed'
=> 100 closed, 96 merged, 96.0% merge rate
[check] step 4 done; advancing to step 5
... (steps 5-6 succeed) ...
[summarize] asking the LLM for a final answer...
[cleanup] deleting sandbox ...
[cleanup] done
============================================================
FINAL ANSWER
============================================================
PR Merge Rate: 96/100 closed PRs merged = 96.0%.
Top 5 PR authors (total PRs, personal merge rate):
nfcampos 40 100.00%
hinthornw 18 100.00%
hwchase17 15 93.33%
rlancemartin 10 100.00%
baskaryan 4 100.00%
Most-commented open issue:
"Long tool calls (~180s+) silently re-executed from checkpoint on LangGraph Cloud"
25 comments, opened by MarioAlessandroNapoli on 2026-04-05.
```
#### What the retry path catches
In the canonical run above no step needs a retry, because the interpreter context's persistent state matches what the LLM expects: variables and imports from earlier steps are still live, so the obvious "I forgot to re-import" class of failure cannot happen. The retry path is still there, and it fires for _genuine_ code failures: a syntax error, a runtime exception (`KeyError`, `TypeError`, `IndexError`), the LLM hallucinating a method that doesn't exist on the response object, a malformed SQL query, an unhandled empty result, and so on.
When a step does fail, `sandbox.code_interpreter.run_code(code)` returns an `ExecutionResult` whose `error` field is set to an `ExecutionError` carrying `name`, `value`, and `traceback`. The `execute` node serializes those fields into `state["last_error"]` and stores the failing source in `state["last_code"]`. `check` sees the error, increments `attempts`, and `route_from_check` sends control back to `execute`. The retry call to the LLM now includes **both** the original step description, the still-in-scope prior steps' code, and the failing code plus the error and traceback, so the LLM can diagnose the problem and produce a materially different fix rather than retrying the same approach. This continues up to `max_attempts` times; past that, `route_from_check` routes to `summarize` with `state["last_error"]` still set, and the summarizer reports the failure honestly instead of fabricating a result.
This is the value the graph provides: **the failure state is explicit, persistent, and visible to every subsequent LLM call**. There's no hidden conversation context, no implicit ReAct loop, no need to trust the prebuilt agent. Every routing decision happens in code you can read.
### 6. Adapting the Example
The graph topology is task-agnostic. To profile a different repository, change the URL in `USER_REQUEST` and rerun. To use a different model, swap `ChatAnthropic` for `ChatOpenAI` (and update `.env.example`). To allow more retries per step, raise `max_attempts` in the initial state dict. To run a different analytical workflow entirely, replace `USER_REQUEST`; the plan-and-execute machinery doesn't care what task it's executing.
**Key advantages of this approach:**
- **Inspectable plan:** The list of steps lives in graph state as data (`state["plan"]`), not implicit in chat history, so you can log it, modify it, or replay any individual step.
- **Explicit retry control:** Failure handling is your code, not a prebuilt agent's black box. `max_attempts`, the routing logic in `route_from_check`, and the prompt context shown on each retry are all readable and tunable.
- **Persistent interpreter context:** Daytona's `code_interpreter.run_code` shares one Python interpreter across all `execute` calls, so imports, variables, and functions defined in earlier steps stay in scope, exactly the behavior the LLM's plan assumes.
- **Secure sandbox execution:** Every line of LLM-generated Python runs in an isolated Daytona sandbox, not on your machine.
- **Task-agnostic topology:** The same six-node graph works for any analytical workflow by swapping `USER_REQUEST`. The provision/plan/execute/check/summarize/cleanup machinery doesn't depend on the specific task.
# Use Mastra Coding Agent with Daytona
This guide demonstrates how to configure the [Mastra coding agent](https://github.com/mastra-ai/template-coding-agent) to use Daytona sandboxes, enabling you to leverage AI capabilities for any coding-related task in a secure, isolated environment.
---
### 1. Workflow Overview
Once configured, you can use Mastra Studio to interact with the coding agent through a ChatGPT-like interface. This enables human-in-the-loop workflows where you can guide the agent, review its decisions, and iterate on solutions in real-time; all while the agent executes tasks securely within Daytona sandboxes.
### 2. Project Setup
:::note[Node.js Version]
Node.js version 20 or higher is required to run the coding agent. Please ensure your environment meets this requirement before proceeding.
:::
#### Clone the Repository
Clone the Mastra coding agent template repository, which includes the agent implementation and Daytona integration:
```bash
git clone https://github.com/mastra-ai/template-coding-agent.git
cd template-coding-agent
```
#### Configure Environment
Create a `.env` file in the project root directory:
```bash
touch .env
```
The `.env` file requires the following configuration:
- **LLM Provider**: The AI model provider for your coding agent
- **Model**: The specific model to use from your chosen provider
- **Sandbox Provider**: Daytona configuration for isolated execution
For this guide, we'll use OpenAI as the LLM provider with the `gpt-4o-mini` model:
```env
OPENAI_API_KEY=your_openai_key
MODEL=openai/gpt-4o-mini
```
Next, configure Daytona as your sandbox provider by adding your API key (available from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys)):
```env
DAYTONA_API_KEY=your-daytona-api-key-here
```
#### Install Dependencies
Install the required packages using pnpm:
```bash
pnpm install
```
### 3. Running the Agent
Mastra Studio provides a ChatGPT-like interface for interacting with your coding agent. This mode offers:
- **Conversation History**: Previous conversations are stored and organized in threads
- **Visual Debugging**: Inspect agent execution steps, workflow, and tool calls
- **Model Switching**: Easily switch between different AI models
- **Tool Inspection**: View which tools your agent is using in real-time
For a complete overview of all features and capabilities, visit the [Mastra Studio documentation](https://mastra.ai/docs/getting-started/studio).
Start the dev server with:
```bash
pnpm run dev
```
If the dev server starts successfully, you'll see the terminal output displaying the URLs where you can access Mastra Studio:
```bash
│ Studio: http://localhost:4111
│ API: http://localhost:4111/api
```
Once started, open the provided URL in your browser to access the interactive interface. You can interact with your agent while monitoring its workflow in the terminal, where detailed logs show execution steps and low-level parameters.
Below is an example of terminal logs generated when the agent calls the `writeFile` tool to create a JavaScript file with a basic "Hello, world!" output:
```json
{
"text": "",
"toolCalls": [
{
"type": "tool-call",
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f",
"from": "AGENT",
"payload": {
"toolCallId": "call_NiLLgBmgrYLSL0MsrG54E4A5",
"toolName": "writeFile",
"args": {
"sandboxId": "2152d23b-5742-47c2-9992-4414d4144869",
"path": "hello.js",
"content": "console.log('Hello, world!');"
},
"providerMetadata": {
"openai": {
"itemId": "fc_00bba3412cd22a2b0069399fbaeef881909b0583f359cbc33c"
}
}
}
}
],
"toolResults": [
{
"type": "tool-result",
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f",
"from": "AGENT",
"payload": {
"args": {
"sandboxId": "2152d23b-5742-47c2-9992-4414d4144869",
"path": "hello.js",
"content": "console.log('Hello, world!');"
},
"toolCallId": "call_NiLLgBmgrYLSL0MsrG54E4A5",
"toolName": "writeFile",
"result": {
"success": true,
"path": "/home/daytona/hello.js"
},
"providerMetadata": {
"openai": {
"itemId": "fc_00bba3412cd22a2b0069399fbaeef881909b0583f359cbc33c"
}
}
}
}
],
"finishReason": "tool-calls",
"usage": {
"inputTokens": 4243,
"outputTokens": 53,
"totalTokens": 4296,
"reasoningTokens": 0,
"cachedInputTokens": 4096
},
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f"
}
```
These logs show complete visibility into tool execution including arguments, results, token usage with caching metrics, and unique identifiers for debugging.
### 4. Example Usage
Let's walk through a practical example to see the Mastra coding agent and Daytona sandbox integration in action. We'll ask the agent to complete the following task:
> _"Create a Python file with a function that reverses a string, write a test file for it, and run the tests."_
#### Agent Workflow
The agent executes this task through a series of coordinated steps, utilizing different tools to interact with the Daytona sandbox:
##### 1. Create Sandbox
The agent first provisions a new Daytona sandbox for Python development:
**Tool:** `createSandbox`
**Arguments:**
```json
{
"name": "reverse_string_project",
"labels": null,
"language": "python",
"envVars": null
}
```
**Result:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e"
}
```
##### 2. Write Project Files
Next, the agent creates both the implementation and test files in a single operation:
**Tool:** `writeFiles`
**Arguments:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e",
"files": [
{
"path": "reverse_string.py",
"data": "def reverse_string(s):\n \"\"\"Return the reversed version of the input string s.\"\"\"\n return s[::-1]\n"
},
{
"path": "test_reverse_string.py",
"data": "import unittest\nfrom reverse_string import reverse_string\n\nclass TestReverseString(unittest.TestCase):\n def test_regular(self):\n self.assertEqual(reverse_string(\"hello\"), \"olleh\")\n def test_empty(self):\n self.assertEqual(reverse_string(\"\"), \"\")\n def test_single_char(self):\n self.assertEqual(reverse_string(\"a\"), \"a\")\n def test_numbers(self):\n self.assertEqual(reverse_string(\"12345\"), \"54321\")\n\nif __name__ == \"__main__\":\n unittest.main()\n"
}
]
}
```
**Result:**
```json
{
"success": true,
"filesWritten": [
"/home/daytona/reverse_string.py",
"/home/daytona/test_reverse_string.py"
]
}
```
##### 3. Execute Tests
Finally, the agent runs the test suite to verify the implementation:
**Tool:** `runCommand`
**Arguments:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e",
"command": "python3 test_reverse_string.py",
"envs": null,
"workingDirectory": null,
"timeoutSeconds": 20,
"captureOutput": true
}
```
**Result:**
```json
{
"success": true,
"exitCode": 0,
"stdout": "....\n----------------------------------------------------------------------\nRan 4 tests in 0.000s\n\nOK\n",
"command": "python3 test_reverse_string.py",
"executionTime": 218
}
```
#### Agent Response
After successfully completing all steps, the agent provides a detailed summary:
```
The project is complete! Here's what was done:
✓ Created reverse_string.py with a function to reverse a string.
✓ Created test_reverse_string.py using Python's unittest to test the function with several cases.
✓ Ran the tests—all tests passed successfully.
Test Output:
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
Would you like to see the code for either file or make any changes?
```
This example demonstrates how the agent autonomously handles the entire development workflow, from environment setup to test execution, all within the secure Daytona sandbox environment.
:::tip[Sandbox Management]
After the agent completes its task, you'll see the active sandbox in your [Daytona Dashboard](https://app.daytona.io/dashboard). It's recommended to clean up resources when finished, unless your use case requires the sandbox to remain active (e.g. for preview URLs or ongoing development).
:::
**Key Advantages**
- **Secure Isolation**: All agent operations run in isolated Daytona sandboxes, protecting your local environment
- **Multi-Language Support**: Execute code across different programming languages without local setup
- **Enhanced Debugging**: Use Mastra Studio to visualize and debug agent workflows in real-time
- **Scalable Execution**: Leverage Daytona's cloud infrastructure for resource-intensive tasks
# Claude Guides
Guides for integrating Claude with Daytona.
# Run Claude Code in a Daytona Sandbox via CLI
This guide walks you through running Claude Code inside a Daytona sandbox using the Daytona CLI.
### Prerequisites
- Daytona account and API key (Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys))
- Local terminal (macOS, Linux, or Windows)
### Install the Daytona CLI
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
:::note
Already have the CLI? Check your version with `daytona --version`. If it's below **0.135.0**, [upgrade to the latest version](https://www.daytona.io/docs/en/getting-started.md#cli).
:::
### Authenticate with Daytona
Log in to your Daytona account using your API key:
```bash
daytona login --api-key=YOUR_API_KEY
```
Replace `YOUR_API_KEY` with your actual Daytona API key.
### Create a Sandbox
Create a new sandbox for running Claude Code:
```bash
daytona sandbox create --name claude-sandbox
```
This creates a sandbox named `claude-sandbox`, visible in your [Dashboard](https://app.daytona.io/dashboard/sandboxes). The default Daytona snapshot includes Claude Code, so the command above is all you need.
:::tip
Need more power? Pass `--snapshot daytona-large` or `--snapshot daytona-medium` flag to increase your sandbox resources. See [default snapshots](https://www.daytona.io/docs/en/snapshots.md#default-snapshots) for resource details.
:::
### Connect to the Sandbox
SSH into your sandbox:
```bash
daytona ssh claude-sandbox
```
You now have an interactive terminal session inside the sandbox.
### Run Claude Code
Inside the SSH session, start Claude Code:
```bash
claude
```
On first run, Claude Code will prompt you to authenticate:
1. Copy the authentication URL displayed in the terminal
2. Open the URL in your local browser
3. Complete the authentication flow
4. Copy the code provided by the browser
5. Paste the code back into the terminal
Once authenticated, you're all set. Claude Code runs inside the sandbox while you control it from your terminal.
# Build a Coding Agent Using Claude Agent SDK and Daytona
import { Image } from 'astro:assets'
import claudeAgentSDKInteractiveTerminalSandboxResult from '../../../../../assets/docs/images/claude-agent-sdk-interactive-terminal-sandbox-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [Claude Code](https://code.claude.com/docs/en/overview) inside a Daytona sandbox environment. The agent uses the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to follow user prompts.
The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and a Python agent is initialized inside it. The agent is based on the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview).
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start 174s
Creating sandbox...
Installing Agent SDK...
Initializing Agent SDK...
Press Ctrl+C at any time to exit.
User: Build a Zelda-like game where I can move around the screen and talk to famous programmers
Thinking...
I'll build a Zelda-like game for you! This will be a fun project with player movement and NPC interactions with famous programmers.
🔨 Write
🔨 Write
Now let me start a simple HTTP server to host the game:
🔨 Bash
Perfect! I've created a Zelda-like game called "Programmer's Quest" for you! 🎮
## Game Features:
✨ Zelda-style gameplay:
- Top-down 2D view with classic retro aesthetics
- Player character with sword and shield
- Grid-based movement system
- Environmental obstacles (trees and rocks)
👥 Famous Programmers as NPCs:
1. Linus Torvalds - Creator of Linux
2. Grace Hopper - COBOL pioneer and Admiral
3. Alan Turing - Father of computer science
4. Ada Lovelace - First computer programmer
5. Dennis Ritchie - Creator of C and UNIX
🎮 Controls:
- Arrow Keys or WASD - Move your character
- SPACE - Talk to NPCs when you're near them
🌟 Gameplay:
- Explore the grassy map and find all 5 legendary programmers
- Each NPC has multiple quotes that cycle when you talk to them
- NPCs glow when you're near them
- Dialog boxes appear with their famous quotes
- Track your progress in the HUD
## Play Now:
🎯 [Click here to play the game!](https://80-8e2c4d23-212a-4f1e-bb6c-abfa71aeed3a.proxy.daytona.works)
The game features smooth movement, collision detection with trees and rocks, and an immersive dialog system. Try to find and talk to all 5 famous programmers to learn their wisdom! Each has 3 different quotes that cycle as you keep talking to them.
Enjoy your adventure! 🗡️✨
User:
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/anthropic/single-claude-agent-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_ANTHROPIC_API_KEY=your_anthropic_key
```
:::caution[API Key Security]
Note: The `SANDBOX_ANTHROPIC_API_KEY` key is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Example Usage
Example chat session:
```
$ npm run start
Creating sandbox...
Installing Agent SDK...
Initializing Agent SDK...
Press Ctrl+C at any time to exit.
User: Create a fun pet store landing page in Svelte
Thinking...
I'll create a fun pet store landing page in Svelte for you! Let me set up the project and build an engaging landing page with animations and a colorful design.
🔨 Bash
🔨 Bash
Now let me create a fun and engaging pet store landing page with animations, colorful sections, and interactive elements:
🔨 Read
> 🔨 Write
Now let me create the component files:
🔨 Write
🔨 Write
🔨 Write
🔨 Write
🔨 Write
Now let's update the main CSS file and start the development server:
🔨 Read
🔨 Write
Now let's start the development server:
🔨 Bash
🔨 TaskOutput
Perfect! 🎉 I've created a fun and colorful pet store landing page in Svelte! Here's what I built:
## Features:
✨ Hero Section
- Beautiful gradient background with animated floating cards
- Eye-catching navigation bar
- Call-to-action buttons with hover effects
- Bouncing emoji animations
🎯 Features Section
- 4 key benefits (Free Delivery, Premium Quality, Special Offers, Award Winning)
- Interactive cards with hover animations
- Icons that rotate and scale on hover
🛍️ Products Section
- 6 featured products with colorful backgrounds
- Star ratings
- Add to cart functionality
- Smooth hover animations
📧 Newsletter Section
- Vibrant gradient background with floating pet emojis
- Email subscription form
- Success message animation
- Special 20% off offer
👣 Footer
- Comprehensive navigation links
- Social media icons
- Dark gradient design
## Live Preview:
Your pet store is now running at: https://80-b3739199-d90e-4bde-ab18-450d74c950e8.proxy.daytona.works
The page includes:
- Smooth scroll behavior
- Responsive design for mobile devices
- Fun animations throughout
- Interactive elements with hover effects
- Colorful gradients and modern styling
- Emoji-based icons for a playful feel
Click the link to see your fun pet store landing page in action! 🐾
User:
```
### 4. Understanding the Agent's Architecture
This example consists of two main components:
- **Main Program:** The main program is a Node.js script (`index.ts`) that runs on your local machine. It uses the Daytona SDK to create and manage a Daytona sandbox. The main program provides a command line interface for interacting with the agent inside the sandbox.
- **Sandbox Agent:** The sandbox agent is a Python script (`coding_agent.py`) that runs inside the Daytona sandbox. It uses the Claude Agent SDK to create a customized coding agent similar to Claude Code.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your Anthropic API key included in the environment variables.
2. Installs the Claude Agent SDK by running `pip install` in the sandbox with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates a new [code interpreter context](https://www.daytona.io/docs/en/process-code-execution.md#run-code-stateful).
4. Uploads the coding agent script to the sandbox with [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
5. Initializes the Claude Agent SDK by running `import coding_agent` in the code interpreter context.
6. Waits for user input and sends prompts to the agent in the code interpreter context as shown below.
#### Main Program Code
Once the agent is running, the program creates a readline interface to read user input and sends it to the agent.
Each user request is passed to the agent by running a Python command in the code interpreter context:
```typescript
const result = await sandbox.codeInterpreter.runCode(
`coding_agent.run_query_sync(os.environ.get('PROMPT', ''))`,
{
context: ctx,
envs: { PROMPT: prompt },
onStdout,
onStderr,
}
)
```
The `onStdout` and `onStderr` callbacks are used to pass the agent's output back to the main program. After the agent finishes responding to the prompt, the main program waits for the next user input.
#### Sandbox Agent Code
The sandbox agent uses the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to create a customized coding agent based on Claude Code.
The agent is initialized with a system prompt that includes the workspace directory and an example of the [preview URL format](https://www.daytona.io/docs/en/preview.md):
```python
system_prompt = """
You are running in a Daytona sandbox.
Use the /home/daytona directory instead of /workspace for file operations.
Your public preview URL for port 80 is: {}.
""".format(preview_url)
```
It also specifies the [tools and permission mode](https://platform.claude.com/docs/en/agent-sdk/quickstart#key-concepts) of the agent:
```python
client = ClaudeSDKClient(
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob", "Grep", "Bash"],
permission_mode="acceptEdits",
system_prompt=system_prompt
)
)
```
The code to run queries and receive responses follows the examples in Anthropic's [Claude Agent Python SDK documentation](https://platform.claude.com/docs/en/agent-sdk/python).
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Communicate with the agent directly in your terminal
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Build a Two-Agent Coding System with Claude and Daytona
This guide demonstrates how to run a two-agent autonomous coding system using the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) and Daytona sandboxes. The system consists of a **Project Manager Agent** (local) and a **Developer Agent** (in-sandbox), enabling advanced delegation, planning, and secure code execution.
The Project Manager Agent runs locally and uses the basic Anthropic interface with the `claude-sonnet-4-20250514` model for high-level planning and task delegation. The Developer Agent runs inside the Daytona sandbox and is created using the Claude Agent SDK, which leverages Claude Code for advanced coding and automation capabilities. This architecture separates high-level planning from low-level code execution for more robust automation.
A key advantage of this approach is its **extensibility**: you can easily replace the Project Manager Agent with your own custom orchestrator logic, or even another agent, making the system highly reusable and adaptable to a wide range of advanced automation and coordination use cases.
---
### 1. Workflow Overview
When the main module is launched, a Daytona sandbox is created for the Developer Agent, and a Project Manager Agent is initialized locally. Interaction with the system occurs via a command line chat interface. The Project Manager Agent receives prompts, plans the workflow, and delegates coding tasks to the Developer Agent. The Developer Agent executes tasks in the sandbox and streams results back to the Project Manager, who reviews and coordinates further actions. All logs and outputs from both agents are streamed in real time to the terminal, providing full visibility into the process as it is managed by the Project Manager Agent.
The Developer Agent can also host web apps and provide preview links using [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md). The Project Manager Agent will present these links and summarize the results for you.
You can continue interacting with the system until you are finished. When you exit the program, the sandbox is deleted automatically.
---
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/anthropic/multi-agent-claude-sdk
```
#### Configure Environment
To run this example, you need to set the following environment variables:
- `DAYTONA_API_KEY`: Required for access to Daytona sandboxes. Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- `ANTHROPIC_API_KEY`: Required for the **Project Manager Agent** (runs locally). Get it from [Claude Developer Platform](https://console.anthropic.com/settings/keys)
- `SANDBOX_ANTHROPIC_API_KEY`: **Optional** for the **Developer Agent** (runs in sandbox). If not provided, defaults to using `ANTHROPIC_API_KEY`. Get it from [Claude Developer Platform](https://console.anthropic.com/settings/keys)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
SANDBOX_ANTHROPIC_API_KEY=your_anthropic_key
```
:::tip[Agent API Key Options]
You can use a single `ANTHROPIC_API_KEY` for both agents, or provide a separate `SANDBOX_ANTHROPIC_API_KEY` for billing/tracking purposes.
:::
:::caution[API Key Security]
The `SANDBOX_ANTHROPIC_API_KEY` is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the two-agent system:
```bash
npm run start
```
The system will start and wait for your prompt.
---
### 3. Example Usage
Example chat session:
```
$ npm run start
Creating Developer Agent sandbox...
Installing Developer Agent SDK...
Initializing Developer Agent...
Initializing Project Manager Agent...
=== Two-Agent System Ready ===
Project Manager Agent: Manages tasks and delegates to Developer (Green text)
Developer Agent: Executes code in Daytona sandbox (White text)
Press Ctrl+C at any time to exit.
User: make a lunar lander web app
[Project Manager] Processing your request...
[Project Manager]: I'll help you create a lunar lander web app! Let me delegate this task to the Developer Agent.
Create a lunar lander web game with the following features:
- HTML/CSS/JavaScript based
- Canvas graphics for the game
- Physics simulation (gravity, thrust, velocity)
- Keyboard controls (arrow keys)
- Landing detection (safe landing vs crash)
- Fuel management system
- Start the game on port 80 and provide the preview URL
[Delegating to Developer Agent]...
I'll help you create a lunar lander web app! Let me build a complete game with physics simulation, graphics, and controls.
🔨 Write
🔨 Write
🔨 Bash
Perfect! The web server is now running.
Your Lunar Lander game is live at:
🌐 https://80-17ac1c0f-d684-4122-93b5-8f52fd5393f8.proxy.daytona.works
[Project Manager]: Excellent! The Developer Agent has successfully created your lunar lander web app. The game is now running and accessible at the preview URL above. The implementation includes:
✓ Physics simulation with gravity and thrust
✓ Canvas-based graphics
✓ Keyboard controls for gameplay
✓ Landing detection system
✓ Fuel management
✓ Web server running on port 80
TASK_COMPLETE
[Project Manager] All tasks completed!
```
---
### 4. Understanding the Agent Architecture
This system is composed of two collaborating agents, each with a distinct role and implementation. Below is a step-by-step guide to how each agent operates and interacts within the workflow.
#### Project Manager Agent (Terminal Orchestration)
1. **User Interaction:**
- All user interaction occurs via the terminal with the Project Manager Agent.
- The Project Manager Agent is configured with a system prompt that defines its role and maintains the full conversation history.
2. **Awareness of Developer Agent:**
- The Project Manager Agent knows that a Developer Agent is available inside a Daytona sandbox and can be invoked as needed.
3. **Task Delegation:**
- When the Project Manager Agent determines that a coding task should be delegated, it encapsulates the task within `` tags in its response.
- The system parses these tags and, when present, invokes the Developer Agent with the specified task.
4. **Iterative Workflow:**
- This process can repeat multiple times, with the Project Manager Agent reasoning about progress and delegating further tasks as needed.
5. **Session Completion:**
- When the Project Manager Agent determines the overall task is complete, it outputs `TASK_COMPLETE`, which signals the system to terminate the session.
#### Developer Agent (Sandbox Execution)
1. **Provisioning:**
- The Developer Agent is provisioned inside a Daytona sandbox and is responsible for executing coding tasks.
2. **SDK Installation:**
- The system installs the Claude Agent SDK in the sandbox by running `pip install` (see [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution)).
3. **Interpreter Context:**
- A new [code interpreter context](https://www.daytona.io/docs/en/process-code-execution.md#run-code-stateful) is created for isolated execution.
4. **Script Upload:**
- The coding agent script is uploaded to the sandbox using [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
5. **SDK Initialization:**
- The Claude Agent SDK is initialized in the interpreter context (e.g., `import coding_agent`).
6. **Task Execution:**
- When a `` is received, the system sends the task to the Developer Agent by running a Python command in the interpreter context:
```typescript
const result = await sandbox.codeInterpreter.runCode(
`coding_agent.run_query_sync(os.environ.get('PROMPT', ''))`,
{
context: ctx,
envs: { PROMPT: task },
onStdout,
onStderr,
}
);
```
- The Developer Agent executes the task, streams output, and returns results to the Project Manager Agent for review and further coordination.
---
### 5. Customization
You can customize the Project Manager Agent's behavior by modifying the system prompt in `src/index.ts`. The current implementation:
- Uses `` tags for delegation
- Automatically reviews Developer Agent outputs
- Says "TASK_COMPLETE" when finished
---
### 6. Cleanup
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
---
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Hierarchical agent architecture for robust automation
- Extensible and reusable architecture
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Running Claude Code with Daytona
Claude Code allows you to automate and orchestrate tasks using natural language and code. With Daytona, you can easily run Claude Code inside isolated sandboxes, making it simple to experiment and execute tasks securely.
## Running Claude Code in a Daytona Sandbox
You can run Claude Code and execute tasks with it directly inside a Daytona sandbox. The following examples show how to set up a sandbox, install Claude Code, run tasks programmatically, and stream logs in real time.
> **Note:** While both sync and async modes support streaming PTY output, `AsyncDaytona` is recommended as it provides automatic background callbacks via `on_data`. The synchronous API requires blocking iteration or manual threading to handle output.
```python
import os
import asyncio
from daytona import AsyncDaytona
async def run_claude_code():
async with AsyncDaytona() as daytona:
sandbox = await daytona.create()
# Define the Claude Code command to be executed
claude_command = "claude --dangerously-skip-permissions -p 'write a dad joke about penguins' --output-format stream-json --verbose"
# Install Claude Code in the sandbox
await sandbox.process.exec("npm install -g @anthropic-ai/claude-code")
pty_handle = await sandbox.process.create_pty_session(
id="claude", on_data=lambda data: print(data.decode(), end="")
)
await pty_handle.wait_for_connection()
# Run the Claude Code command inside the sandbox
await pty_handle.send_input(
f"ANTHROPIC_API_KEY={os.environ['ANTHROPIC_API_KEY']} {claude_command}\n"
)
# Use this to close the terminal session if no more commands will be executed
# await pty_handle.send_input("exit\n")
await pty_handle.wait()
# If you are done and have closed the PTY terminal, it is recommended to clean up resources by deleting the sandbox
# await sandbox.delete()
if __name__ == "__main__":
asyncio.run(run_claude_code())
````
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
try {
const sandbox = await daytona.create();
// Define the Claude Code command to be executed
const claudeCommand =
"claude --dangerously-skip-permissions -p 'write a dad joke about penguins' --output-format stream-json --verbose";
// Install Claude Code in the sandbox
await sandbox.process.executeCommand("npm install -g @anthropic-ai/claude-code");
const ptyHandle = await sandbox.process.createPty({
id: "claude",
onData: (data) => {
process.stdout.write(data);
},
});
await ptyHandle.waitForConnection();
// Run the Claude Code command inside the sandbox
ptyHandle.sendInput(
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY} ${claudeCommand}\n`
);
// Use this to close the terminal session if no more commands will be executed
// ptyHandle.sendInput("exit\n")
await ptyHandle.wait();
// If you are done and have closed the PTY terminal, it is recommended to clean up resources by deleting the sandbox
// await sandbox.delete();
} catch (error) {
console.error("Failed to run Claude Code in Daytona sandbox:", error);
}
````
# Run Claude Managed Agents on Daytona
import { Image } from 'astro:assets'
import claudeManagedAgentsArchitecture from '../../../../../assets/docs/images/claude-managed-agents-architecture.svg'
A guide to running Claude Managed Agents inside your own Daytona sandboxes, as a self-hosted environment.
### Introduction
Claude Managed Agents is Anthropic's configurable agent harness and infrastructure for running Claude as an autonomous agent. You define an agent (model, system prompt, tools, MCP servers), open a session, and stream events while the agent reads files, runs commands, and uses other tools to finish the task. By default, sessions run inside Anthropic-operated cloud containers.
A *self-hosted environment* moves that container layer to you. Everything else you get from Managed Agents stays unchanged: the agent loop, prompt caching, model calls, event stream, and session history all stay on Anthropic's side. The container that holds the agent's filesystem and shell runs on your infrastructure. With the Daytona integration, that container is a Daytona sandbox.
### How the pieces fit together
Three parties are involved in any session:
- **Anthropic** runs the API, the agent loop, and a per-environment work queue that signals when an agent has tools to dispatch.
- **You** run two things: an *application* that creates sessions and talks to your end users, and an *orchestrator* that manages the sandbox lifecycle (create, start, stop, clean up) and runs the agent's tool runner inside each sandbox.
- **Daytona** provides the sandbox containers in which filesystem and shell tools execute.
When the agent decides to use a tool, where the call goes depends on the tool:
- **Filesystem and shell tools** (`bash`, `read`, `write`, `edit`, `glob`, `grep`) are dispatched inside your Daytona sandbox. Your orchestrator ensures the agent's tool runner is running there; the runner executes each call against the sandbox's filesystem and shell and posts the result back to the session stream.
- **Web tools** (`web_search`, `web_fetch`) and **MCP server tools** are dispatched by Anthropic server-side. MCP calls use credentials held in Anthropic-managed vaults. The sandbox is not involved.
This split means that a self-hosted environment changes where filesystem and shell tools run, and nothing else.
Each session gets its own isolated sandbox: filesystem changes persist across tool calls within the session, and while a single runner is alive the bash shell also keeps its working directory, environment, and background processes between calls.
### What you set up
#### Reference implementation
A reference implementation of the orchestrator, the in-sandbox runner, the snapshot builder, and example agents is provided in the Daytona repo at [`guides/python/claude/claude-managed-agents/`](https://github.com/daytonaio/daytona/tree/main/guides/python/claude/claude-managed-agents). Each section below points to the concrete file in that directory.
To follow along locally:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/python/claude/claude-managed-agents
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .
```
If you plan to run the reference webhook orchestrator, install with the `webhook` extras instead: `pip install -e ".[webhook]"`.
#### On the Anthropic side
1. **Create a self-hosted environment.** In the Claude Console, open *Workspace → Environments → New → Self-hosted*, or from code:
```python
import anthropic
env = anthropic.Anthropic().beta.environments.create(
name="my-daytona-env",
config={"type": "self_hosted"}
)
```
2. **Generate an environment key** for the environment from the Console. This key authenticates the whole worker flow (poll, ack, stop, heartbeat, session event stream, skill download) for this one environment. Keep it on the orchestrator host only.
3. **Create your agent** as you would for any Managed Agents setup. The agent does not need to know it will run on a self-hosted environment; that is decided per session. The reference's `create_agent.py ` is a one-liner for spinning up a sandbox-tools-only testing agent and printing its id.
#### On the Daytona side
Daytona provides the per-session sandbox containers and the snapshot mechanism your orchestrator uses. You'll need a Daytona account and API key.
The reference's `build_default_snapshot.py` builds a sandbox image from `Dockerfile.default` and publishes it as a snapshot in your Daytona workspace. The Dockerfile mirrors the runtimes from Claude Managed Agents' [container reference](https://platform.claude.com/docs/en/managed-agents/cloud-containers). Run the script once; from then on, the orchestrator creates a sandbox per session from that snapshot, on demand.
At its core the script wraps a single Daytona SDK call:
```python
from daytona import CreateSnapshotParams, Daytona, Image, Resources
Daytona().snapshot.create(
CreateSnapshotParams(
name="byoc-env-default",
image=Image.from_dockerfile("Dockerfile"),
resources=Resources(cpu=2, memory=8, disk=10),
),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
To change what's installed in the sandbox, edit `Dockerfile.default` and rerun the script. It hashes the Dockerfile, names the snapshot `byoc-env-default-`, and no-ops if a snapshot with that exact hash already exists.
### Running the orchestrator
#### The orchestrator process
You run an orchestrator as a long-lived process. Its responsibilities:
- Watch the environment's work queue, either by long-polling it or by receiving webhooks from Anthropic on each new turn.
- For each work item, ensure a Daytona sandbox is running and start the agent's tool runner inside it. The runner attaches to the session's event stream and answers `bash`, `read`, `write`, `edit`, `glob`, `grep` against the sandbox.
- Stop sandboxes that have gone idle past a configurable threshold, and start them back up on the next work item. The sandbox's filesystem survives the pause, so a session can sit quiet between bursts of activity without keeping a sandbox running. Sandboxes that stay stopped for 30 days are deleted; activity restarts the timer.
- Archive sandboxes when their session terminates. The filesystem stays in Daytona's cost-effective object storage until the same 30-day window expires, then the sandbox is deleted.
The reference includes two orchestrator variants:
- `host_orchestrator_polling.py` long-polls the work queue and only needs the environment key, so it works against environments behind any kind of NAT or firewall.
- `host_orchestrator_webhook.py` is a FastAPI receiver that drains the queue on each `session.status_run_started` delivery; it needs a publicly reachable URL and an `ANTHROPIC_WEBHOOK_SECRET`, but it avoids continuous polling.
Both share the same sandbox-lifecycle logic.
The per-work-item path looks roughly like this (the reference's `orchestrator_lib.py` adds dedupe, ack, retries, locking, and the janitor thread):
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, DaytonaNotFoundError
daytona = Daytona()
def handle_work(work):
session_id = work.data.id
name = f"byoc-{session_id}"
try:
sb = daytona.get(name)
except DaytonaNotFoundError:
sb = daytona.create(CreateSandboxFromSnapshotParams(
name=name,
snapshot="byoc-env-default",
labels={"byoc.session_id": session_id},
))
if sb.state in ("stopped", "archived"):
sb.start()
sb.fs.upload_file(open("sandbox_runner.py", "rb").read(),
"/home/daytona/sandbox_runner.py")
sb.process.exec("pip install --user anthropic")
sb.process.exec(
f"ANTHROPIC_ENVIRONMENT_KEY={environment_key} "
f"ANTHROPIC_WORK_ID={work.id} ANTHROPIC_SESSION_ID={session_id} "
f"ANTHROPIC_ENVIRONMENT_ID={environment_id} "
"nohup python3 /home/daytona/sandbox_runner.py &"
)
```
The orchestrator needs the environment key and a Daytona API key (plus an `ANTHROPIC_WEBHOOK_SECRET` if you run the webhook receiver). It does not need any user-facing credentials, and it does not need to know about your application.
#### The in-sandbox runner
The "agent's tool runner" the orchestrator launches is a small Python process. The Anthropic SDK ships an `EnvironmentWorker` that composes skill download, tool dispatch, heartbeating the work-item lease, and the force-stop on exit. All the runner has to do is call `handle_item()`, for example:
```python
import asyncio
import os
from anthropic import AsyncAnthropic
async def main():
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
async with AsyncAnthropic(auth_token=environment_key) as client:
await client.beta.environments.work.worker(
environment_key=environment_key,
workdir="/mnt/session",
).handle_item()
asyncio.run(main())
```
`handle_item()` reads `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, and `ANTHROPIC_ENVIRONMENT_ID` from the environment, then attaches to the session's event stream and runs the agent's tool calls against the six filesystem and shell tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`) rooted at `workdir`. It heartbeats the work-item lease while the agent runs, and force-stops the lease on exit.
The environment key (passed in by the orchestrator) is the only credential this process needs to talk to Anthropic. It's scoped to a single environment: the runner can act on sessions in that environment, and nothing else in your Anthropic account. For multi-tenant deployments, give each tenant its own environment.
### Running applications
#### Driving a session
It's the same as for a cloud environment; the only difference is `environment_id` points at your `self_hosted` environment.
```python
session = client.beta.sessions.create(
agent=agent_id,
environment_id="env_01..." # your self-hosted env
)
```
Open the events stream before sending the first user message:
```python
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "what's installed in this container? versions please"}]}],
)
for ev in stream:
# render events as they arrive;
# session.status_idle marks end of turn.
...
```
See [Events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for the full event vocabulary and richer examples.
#### Mounting session resources
You are responsible for mounting session resources, for example files or GitHub repositories. Shared dependencies can be baked into your container image. For per-session state, the orchestrator reads two keys off `session.metadata`:
- `daytona.snapshot_name`: create this session's sandbox from a named Daytona snapshot instead of your default. Use this when different sessions need different toolchains or pre-installed packages.
- `daytona.sandbox_id`: attach an already-prepared Daytona sandbox instead of creating one. Use this when you need to seed per-session state before the agent starts, like cloning a repo, loading a dataset, or mounting a customer-specific volume.
The two keys are mutually exclusive.
```python
session = client.beta.sessions.create(
agent=agent_id,
environment_id="env_01...",
metadata={"daytona.sandbox_id": ""},
)
```
For the prepared-sandbox path, create the sandbox with the right labels before passing its id in `session.metadata`:
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
sb = Daytona().create(CreateSandboxFromSnapshotParams(
snapshot="byoc-env-default",
labels={
"byoc.environment_id": "env_01...",
"byoc.mode": "prepared",
},
))
# do any per-session prep here (clone a repo, load data, ...)
# then pass sb.id as session.metadata["daytona.sandbox_id"]
```
`sb.set_labels({...})` does the same on an already-created sandbox, if you'd rather prepare first and label last.
When the session arrives the orchestrator validates the labels, binds the sandbox by setting `byoc.session_id` and flipping `byoc.mode` to `in-sandbox`, then installs and starts its runner. The sandbox can be in any state at handoff; the orchestrator starts it first.
Custom snapshots and prepared sandboxes must include the runner prerequisites the in-sandbox worker needs. A minimal working Daytona snapshot, included in the reference as `Dockerfile.minimal`, is just:
```dockerfile
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
util-linux procps mawk \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /home/daytona /mnt/session \
&& chmod 777 /home/daytona /mnt/session
WORKDIR /mnt/session
```
:::note
Claude Managed Agents memory stores are not yet supported.
:::
#### MCP servers and vaults
MCP servers and Anthropic-managed vaults work on self-hosted environments without changes. The agent declares the MCP server in its `mcp_servers` list, vault-held credentials are referenced by id, and the call is proxied by Anthropic server-side. Your sandbox is not in the path. This is what lets a single agent mix sandbox-routed tools (a `bash` against your Daytona sandbox) with MCP-proxied tools (a query against, say, Linear) on one event stream.
### What you get
- *The sandbox is yours to do whatever else you want with.* Beyond running the agent's tool runner, you can shell into the container, mount volumes, pre-install per-customer code, warm caches, run sidecar processes, attach observability. The runner is one process in a container that you own end to end.
- *Build your sandbox snapshot however you like.* From a Dockerfile or Daytona's [declarative builder](https://www.daytona.io/docs/en/declarative-builder.md): whatever language versions, system packages, or in-house tools the agent needs goes in there.
- *Switching to cloud is one line.* Point `environment_id` at a cloud environment and the `sessions.create` / `events.stream` loop is unchanged. The `session.metadata` keys are the only Daytona-specific part — drop them and use the cloud equivalents: [environment setup](https://platform.claude.com/docs/en/managed-agents/environments) for the container customization, and the [Files API](https://platform.claude.com/docs/en/managed-agents/files) for per-session inputs.
### See also
- [Claude Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview)
- [Claude Managed Agents tools](https://platform.claude.com/docs/en/managed-agents/tools)
- [Claude Managed Agents events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming)
- [Daytona documentation](https://www.daytona.io/docs/index.md)
# OpenClaw Guides
Guides for running OpenClaw with Daytona.
# Run OpenClaw in a Daytona Sandbox via CLI
This guide walks you through setting up [OpenClaw](https://openclaw.ai/) inside a Daytona sandbox and configuring Telegram and WhatsApp channels.
Running OpenClaw in a Daytona sandbox keeps your AI assistant isolated from your local machine, provides a secure environment for code execution, and ensures your bot stays online 24/7 without tying up your personal computer.
### Prerequisites
- Daytona account and API key (Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys))
- Local terminal (macOS, Linux, or Windows)
### Install the Daytona CLI
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
:::note
Already have the CLI? Check your version with `daytona --version`. If it's below **0.135.0**, [upgrade to the latest version](https://www.daytona.io/docs/en/getting-started.md#cli).
:::
### Authenticate with Daytona
Log in to your Daytona account using your API key:
```bash
daytona login --api-key=YOUR_API_KEY
```
Replace `YOUR_API_KEY` with your actual Daytona API key.
### Create a Sandbox
Create a sandbox for running OpenClaw:
```bash
daytona sandbox create --name openclaw --snapshot daytona-medium --auto-stop 0
```
OpenClaw comes preinstalled in the default Daytona snapshot, so the command above is all you need.
:::note
The `--auto-stop 0` flag disables automatic shutdown, keeping OpenClaw accessible until you manually stop or delete the sandbox. The `daytona-medium` snapshot is required because the OpenClaw gateway needs a minimum of 2GB memory.
:::
### Connect to the Sandbox
SSH into your sandbox:
```bash
daytona ssh openclaw
```
### Run OpenClaw Onboarding
Start the onboarding process:
```bash
openclaw onboard
```
:::note
The model provider steps below are for Anthropic. If using a different provider, follow the prompts for your chosen option.
:::
Follow the prompts:
1. **Security acknowledgment:** Accept to continue
2. **Onboarding mode:** Select **Quickstart**
3. **Model/auth provider:** Select **Anthropic**
4. **Anthropic auth method:** Select **Anthropic API key**
5. **Enter Anthropic API key:** Paste your API key
6. **Default model:** Keep current (default: `anthropic/claude-opus-4-5`)
7. **Select channel:** Choose **Skip for now** (we'll configure channels later)
8. **Configure skills:** Select **No** (configure later based on your needs)
9. **Enable hooks:** Select **Skip for now** (configure later based on your needs)
10. **Gateway service:** Select **Skip** (already installed)
When onboarding finishes, the output will display a **Dashboard ready** section with a dashboard link. Your gateway token is the value after `?token=` in the URL. Save this token - you'll need it to connect to the dashboard.
Also, OpenClaw will ask you to **Install shell completion script?** - choose whatever you prefer, this is optional and doesn't affect functionality.
### Start the Gateway
Run the gateway in the background:
```bash
nohup openclaw gateway run > /tmp/gateway.log 2>&1 &
```
The `&` runs the gateway as a background process, keeping your terminal free for other commands. The `nohup` ensures the gateway keeps running even after you close the SSH connection.
### Access the Dashboard
The OpenClaw dashboard is a web interface for managing your assistant, monitoring connections, and configuring channels. To access it, you need a [preview URL](https://www.daytona.io/docs/en/preview.md) that exposes the gateway port running inside your sandbox.
In your local terminal (not inside the sandbox SSH session), generate the preview URL:
```bash
daytona preview-url openclaw --port 18789
```
This command generates a [signed preview URL](https://www.daytona.io/docs/en/preview.md#signed-preview-url) that securely exposes the port.
Open the URL in your browser, go to the **Overview** section, paste your gateway token in the **Gateway Token** field, and click **Connect**.
:::tip
The preview URL expires after 1 hour by default (customizable with `--expires` flag). When it expires, simply run the same CLI command to generate a new one.
:::
### Pair Your Browser
OpenClaw uses device pairing as a security measure - only approved devices can connect to and control your assistant. When you first attempt to connect from the dashboard, your browser registers as a new device that needs approval.
List pending device requests:
```bash
openclaw devices list
```
Approve your device:
```bash
openclaw devices approve REQUEST_ID
```
Replace `REQUEST_ID` with the value from the **Request** column.
Click **Connect** again in the dashboard.
Once connected, you should see a green status indicator - your OpenClaw is now ready to use.
### Security
Running OpenClaw this way provides three layers of security:
1. **Preview URL:** Time-limited access to the dashboard port
2. **Gateway token:** Required to authenticate with the dashboard
3. **Device approval:** Only approved devices can connect and control your assistant
Even if someone obtains your dashboard URL, they cannot connect without the gateway token and an approved device.
:::caution
Keep your gateway token and preview URL secret. Do not share them publicly.
:::
---
### Configure Telegram
Set up a Telegram bot to chat with OpenClaw.
#### Create a Telegram Bot
1. Open Telegram and search for **@BotFather**
2. Send `/start`, then `/newbot`
3. Enter a name for your bot
4. Enter a username for your bot
5. Copy the bot token provided
#### Configure OpenClaw
Enable Telegram and set your bot token:
```bash
openclaw config set channels.telegram.enabled true
openclaw config set channels.telegram.botToken YOUR_BOT_TOKEN
```
Verify the configuration:
```bash
openclaw config get channels.telegram
```
#### Restart the Gateway
```bash
openclaw gateway stop
nohup openclaw gateway run > /tmp/gateway.log 2>&1 &
```
#### Complete Verification
1. Open your bot's chat in Telegram and click **Start**
2. A pairing code will appear. Approve the pairing request:
```bash
openclaw pairing approve telegram PAIRING_CODE
```
You can now message your OpenClaw through Telegram.
---
### Configure WhatsApp
Set up WhatsApp to chat with OpenClaw.
#### Run Configuration
```bash
openclaw config --section channels
```
When prompted:
1. Select **Local (this machine)** for gateway location
2. Choose **Configure/link**
3. Select **WhatsApp (QR link)**
4. Select **Yes** for "Link WhatsApp now (QR)?"
#### Scan the QR Code
Open WhatsApp on your phone, go to **Settings → Linked Devices → Link a Device**, and scan the QR code displayed in your terminal.
Once paired, you'll see:
```
✅ Linked after restart; web session ready.
```
#### Set Up Your Phone Number
Select **This is my personal phone number** (or choose the other option if you have a separate phone for OpenClaw) and enter your phone number when prompted.
#### Finish Configuration
When prompted to select another channel, choose **Finished**. You'll see:
```
└ Configure complete.
```
#### Start Chatting
Send a message to yourself in WhatsApp - OpenClaw will respond. You can give it instructions and information on how to behave directly in the chat.
:::tip
To allow other users to chat with OpenClaw, add their phone numbers to the **Allow From** list in **Channels → WhatsApp** inside the dashboard. When they send a message, OpenClaw will respond.
:::
# Run OpenClaw in a Daytona Sandbox via SDK
import { Image } from 'astro:assets'
import openclawSandbox from '../../../../../assets/docs/images/openclaw-sandbox.gif'
This guide shows how to run [OpenClaw](https://openclaw.ai/) inside a Daytona sandbox using the Daytona SDK. The script automatically creates and configures a sandbox with OpenClaw and provides an authenticated [preview URL](https://www.daytona.io/docs/en/preview.md) for using OpenClaw in the browser.
---
### 1. Workflow Overview
When you run the script, it creates a Daytona sandbox, starts the OpenClaw gateway inside it, and prints a preview link for the dashboard:
```
$ npm start
Creating Daytona sandbox...
Configuring OpenClaw...
Starting OpenClaw...
(Ctrl+C to shut down and delete the sandbox)
🔗 Secret link to Control UI: https://18789-xxxx.proxy.daytona.works?token=...
```
Open the provided link in your browser to connect to the OpenClaw Control UI. This link contains a configuration token, and anyone can use it to connect to OpenClaw without device approval.
You can use the Control UI to chat with your assistant, configure Telegram and WhatsApp, and manage sessions. When you exit the script (Ctrl+C), the sandbox will not be deleted unless [sandbox persistence is disabled](https://www.daytona.io/docs/en/guides/openclaw/openclaw-sdk-sandbox.md#4-key-constants).
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytonaio/daytona) and go to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/openclaw
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your Daytona API key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
A default OpenClaw configuration is stored in `openclaw.json`. You can customize it according to the [configuration reference](https://docs.openclaw.ai/gateway/configuration-reference). You can also add additional environment variables to `.env.sandbox` (e.g. `ANTHROPIC_API_KEY` for Claude) and they will be loaded into the sandbox.
#### Run the Example
:::note[Node.js]
Node.js 18 or newer is required.
:::
Install dependencies and run:
```bash
npm install
npm start
```
The script creates the sandbox, starts the OpenClaw gateway, and prints a secret link with the token in the URL.
### 3. How It Works
1. The script creates a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with `DAYTONA_SNAPSHOT` (e.g. `daytona-medium`) and loads env vars from `.env.sandbox`.
2. Your local `openclaw.json` is merged with built-in config and written to `~/.openclaw/openclaw.json` in the sandbox.
3. The OpenClaw gateway is started inside the sandbox on `OPENCLAW_PORT` via [process execution](https://www.daytona.io/docs/en/process-code-execution.md).
4. A signed [preview link](https://www.daytona.io/docs/en/preview.md) is generated and the token is appended as `?token=...`; this link is printed so you can open the Control UI.
5. On Ctrl+C, the sandbox is deleted unless `PERSIST_SANDBOX` is `true`.
### 4. Key Constants
You can change behavior by editing the constants in [`src/index.ts`](https://github.com/daytonaio/daytona/blob/main/guides/typescript/openclaw/src/index.ts):
| Constant | Default | Description |
|----------|---------|-------------|
| `PERSIST_SANDBOX` | true | When true, the sandbox is not deleted when the script exits |
| `MAKE_PUBLIC` | true | Allow anyone to access the sandbox instead of limiting to your Daytona organization |
| `OPENCLAW_PORT` | 18789 | OpenClaw Gateway and Control UI port |
| `SHOW_LOGS` | true | Stream OpenClaw stdout/stderr to the terminal |
**Key advantages:**
- Secure, isolated execution in a Daytona sandbox
- No device approval — token in URL and `allowInsecureAuth` skip pairing
- Control UI and channels accessible via the secret preview link
- Optional: keep the sandbox running after exit (`PERSIST_SANDBOX`)
# Build a Multi-Language Benchmark Agent with the Vercel AI SDK and Daytona
import { Image } from 'astro:assets'
import sieveBenchmarkPlot from '../../../../../assets/docs/images/vercel-ai-sdk-sieve-benchmark.png'
This guide walks through building a multi-language benchmarking agent with the [Vercel AI SDK](https://ai-sdk.dev/) and [Daytona](https://www.daytona.io/docs/en/sandboxes.md) sandboxes. The agent has five tools (`runCode`, `runCommand`, `writeFile`, `readFile`, `downloadFile`) and a concrete benchmarking task: implement the Sieve of Eratosthenes in both Python and TypeScript, time them across input sizes, render a matplotlib comparison chart, and deliver two tangible artifacts to your local disk: the chart as a PNG and the findings as a markdown report.
This is the shape of a real coding agent (multi-step, multi-language, multi-tool) producing tangible deliverables rather than just text, running entirely inside Daytona's isolated environment.
---
### 1. Workflow Overview
When you run the script, the agent receives a Sieve of Eratosthenes benchmark task, implements it in two languages, plots the result, and pulls the PNG to your local disk. The transcript below is **excerpted from an actual run** using `claude-sonnet-4-6` against a real Daytona sandbox. Source-code arguments inside `runCode` and `writeFile` calls are abridged (marked by `...`); tool-call shapes, results, file sizes, and the final answer are verbatim:
```
$ npm run start
Creating sandbox...
Prompt:
Implement the Sieve of Eratosthenes (find all primes up to N) in both Python and TypeScript.
Benchmark each across N = 1_000, 10_000, 100_000, and 1_000_000.
Produce two artifacts on my local disk: ./sieve_benchmark.png with a chart
comparing the languages, and ./findings.md with a markdown summary of your
findings.
Keep your final answer concise; the full report is in the downloaded files.
Running agent...
--- Step 1: runCode ---
{ "code": "# Python Sieve of Eratosthenes benchmark ...", "language": "python" }
--- Step 1: result ---
{"exitCode":0,"output":"N= 1,000: median=0.0040 ms\nN= 10,000: median=0.0170 ms\nN= 100,000: median=0.1329 ms\nN=1,000,000: median=1.5081 ms\n"}
--- Step 2: runCode ---
{ "code": "// TypeScript Sieve of Eratosthenes benchmark ...", "language": "typescript" }
--- Step 2: result ---
{"exitCode":0,"output":"N= 1000: median=0.0012 ms\nN= 10000: median=0.0059 ms\nN= 100000: median=0.0786 ms\nN= 1000000: median=1.0883 ms\n"}
--- Step 3: runCode ---
{ "code": "import matplotlib ... plt.savefig('/tmp/sieve_benchmark.png')", "language": "python" }
--- Step 3: result ---
{"exitCode":0,"output":"saved\n"}
--- Step 4: writeFile ---
{ "path": "/tmp/findings.md", "content": "# Sieve of Eratosthenes - Benchmark Findings ..." }
--- Step 4: result ---
{"path":"/tmp/findings.md","bytes":2546}
--- Step 5: downloadFile --- # ← parallel: chart → local disk
{ "remotePath": "/tmp/sieve_benchmark.png", "localPath": "./sieve_benchmark.png" }
--- Step 5: downloadFile --- # ← parallel: findings → local disk
{ "remotePath": "/tmp/findings.md", "localPath": "./findings.md" }
--- Step 5: result ---
{"localPath":"./sieve_benchmark.png","bytes":116848}
--- Step 5: result ---
{"localPath":"./findings.md","bytes":2546}
--- Step 6: text ---
Both files are on your disk. Here's the concise summary:
## Results at a glance
| N | Python (ms) | TypeScript (ms) | TS speed-up |
|----------:|------------:|----------------:|---------------------|
| 1,000 | 0.0040 | 0.0012 | **3.3x** |
| 10,000 | 0.0170 | 0.0059 | **2.9x** |
| 100,000 | 0.1329 | 0.0786 | **1.7x** |
| 1,000,000 | 1.5081 | 1.0883 | **1.4x** |
TypeScript/V8 wins everywhere, but the advantage shrinks from ~3x to ~1.4x
as N grows. At N = 1M both implementations hit the same RAM-bandwidth wall,
erasing most of V8's JIT advantage. Python holds its own because the inner
marking loop is replaced by a C-level bytearray slice assignment; a naive
inner loop would be 10-50x slower. Both scale as O(N log log N).
(full report in ./findings.md)
Cleaning up sandbox...
```
After the run completes, you'll find **two real files** in your project directory:
- `sieve_benchmark.png` (~117 KB): the matplotlib chart comparing both languages
- `findings.md` (~2.5 KB): the structured benchmark report (methodology + raw numbers + conclusion)
The chart the agent drew on that run looks like this:
Worth noticing in the transcript:
- **Each "Step N" label is one agent turn, not one tool call.** Within a turn the model can emit any combination of text and tool calls. Step 6 is the only text-only turn (the final answer), so it gets a clean `--- Step 6: text ---` block. The other turns each have a single tool call.
- **Step 5 batches parallel tool calls.** Both `downloadFile` calls go out in the same turn and the Vercel AI SDK runs them concurrently, so the chart and the report ship to local disk at the same time rather than back-to-back.
- **Steps 1, 2, 3 all use `runCode`** to execute Python, TypeScript, and Python (matplotlib) in turn. No `runCommand` was needed for the TypeScript step because `runCode` handles `ts-node` invocation internally. The exact step count and tool mix will vary run to run; some runs may use `runCommand` if the agent decides to `pip install` an extra package or inspect the filesystem before plotting.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytonaio/daytona) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/vercel-ai-sdk/multi-language-benchmark-agent
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
### 3. How It Works
The example consists of a single Node.js script (`src/index.ts`) that runs on your local machine. It uses the Daytona SDK to manage a sandbox and the Vercel AI SDK's `ToolLoopAgent` to drive the multi-step tool-calling loop.
#### Defining the Five Tools
Each tool wraps a single Daytona primitive and has a single, non-overlapping job. The agent never has to choose between two paths for the same goal.
```typescript
const runCode = tool({
description:
'Execute Python, JavaScript, or TypeScript source code in the sandbox. Pass standalone code; no project setup is required for any language. Use print() / console.log() to surface values. Returns the exit code and combined stdout/stderr.',
inputSchema: z.object({
code: z.string().describe('Source code to execute.'),
language: z.enum(['python', 'javascript', 'typescript']).describe('Language of the code.'),
}),
execute: async ({ code, language }) => {
const ext = { python: 'py', javascript: 'js', typescript: 'ts' }[language]
const path = `/tmp/_run_${randomUUID()}.${ext}`
await sandbox!.fs.uploadFile(Buffer.from(code, 'utf-8'), path)
const cmd = {
python: `python3 ${path}`,
javascript: `node ${path}`,
typescript:
`ts-node --transpile-only --skipProject ` +
`--compilerOptions '{"module":"commonjs","moduleResolution":"node"}' ` +
path,
}[language]
const r = await sandbox!.process.executeCommand(cmd)
return { exitCode: r.exitCode, output: r.result }
},
})
const runCommand = tool({
description: 'Execute a bash shell command in the sandbox. Use for installing packages (pip, npm), running shell utilities (ls, head, wc, find), or chaining commands with pipes.',
inputSchema: z.object({
command: z.string().describe('Shell command to execute.'),
}),
execute: async ({ command }) => {
const r = await sandbox!.process.executeCommand(command)
return { exitCode: r.exitCode, output: r.result }
},
})
const writeFile = tool({
description: 'Write text content to a file in the sandbox filesystem. Use for non-code artifacts: markdown reports, JSON data, config files. Overwrites any existing file at the path.',
inputSchema: z.object({
path: z.string().describe('Absolute path of the file to write.'),
content: z.string().describe('UTF-8 text content to write.'),
}),
execute: async ({ path, content }) => {
await sandbox!.fs.uploadFile(Buffer.from(content, 'utf-8'), path)
return { path, bytes: Buffer.byteLength(content, 'utf-8') }
},
})
const readFile = tool({
description: 'Read a file from the sandbox filesystem and return its contents as text.',
inputSchema: z.object({
path: z.string().describe('Absolute path of the file to read.'),
}),
execute: async ({ path }) => {
const buf = await sandbox!.fs.downloadFile(path)
return { content: buf.toString('utf-8') }
},
})
const downloadFile = tool({
description: 'Download a file from the sandbox to the local filesystem (where this script runs). Use this to extract generated artifacts (plots, datasets, reports) so they remain available after the sandbox is destroyed.',
inputSchema: z.object({
remotePath: z.string().describe('Absolute path of the file in the sandbox.'),
localPath: z.string().describe('Path on the local filesystem to write the downloaded bytes to.'),
}),
execute: async ({ remotePath, localPath }) => {
const buf = await sandbox!.fs.downloadFile(remotePath)
writeFileSync(localPath, buf)
return { localPath, bytes: buf.length }
},
})
```
The tools split the agent's surface into three clean categories:
| Category | Tools | When to use |
|---|---|---|
| **Code execution** | `runCode` | The agent wrote application code - benchmark, algorithm, plotting script. Picks Python/JavaScript/TypeScript per call. |
| **Shell utilities** | `runCommand` | Operating the sandbox itself - `pip install`, `npm install`, `ls`, `wc`. Never used to run scripts the agent wrote. |
| **Filesystem** | `writeFile`, `readFile`, `downloadFile` | Save text artifacts to the sandbox, read them back, extract any file (binary or text) to local disk. |
#### Defining the Agent
The `ToolLoopAgent` is constructed once with the model, `instructions` (the agent's persona and methodology), the tools, and a `stopWhen` condition. The same instance can later be invoked via `.stream()` (used here for live progress output) or `.generate()` (which waits and returns the whole result at the end):
```typescript
const MODEL = anthropic('claude-sonnet-4-6')
const agent = new ToolLoopAgent({
model: MODEL,
instructions: INSTRUCTIONS,
tools: { runCode, runCommand, writeFile, readFile, downloadFile },
stopWhen: stepCountIs(25),
})
const stream = await agent.stream({ prompt: BENCHMARK_PROMPT })
```
The benefit over inline `generateText({ tools, prompt, ... })` shows up when this same agent is later exposed from an HTTP route or shared between a CLI and a UI: the configuration lives in one place, and `createAgentUIStreamResponse({ agent, uiMessages })` plugs it directly into a Next.js route handler that returns a streaming `Response`.
#### Streaming the Agent's Progress
`agent.stream(...)` returns a result whose `fullStream` is an async iterable of typed events: every tool call, every tool result, every text chunk, and any errors surface as they happen. The example iterates that stream with a `for await` loop and a `switch` on `part.type` to print each event with a `Step N` label that matches what you saw in the transcript above:
```typescript
let stepNum = 0
for await (const part of stream.fullStream) {
switch (part.type) {
case 'start-step':
stepNum++
break
case 'tool-call': {
const input = JSON.stringify(part.input, null, 2)
const preview = input.length > 600 ? input.slice(0, 600) + '\n... (truncated)' : input
console.log(`\n--- Step ${stepNum}: ${part.toolName} ---\n${preview}`)
break
}
case 'tool-result': {
const out = JSON.stringify(part.output)
const preview = out.length > 400 ? out.slice(0, 400) + '... (truncated)' : out
console.log(`--- Step ${stepNum}: result ---\n${preview}`)
break
}
case 'tool-error': {
const message = part.error instanceof Error ? part.error.message : JSON.stringify(part.error)
console.error(`\n--- Step ${stepNum}: ${part.toolName} ERROR ---\n${message}`)
break
}
case 'text-start':
console.log(`\n--- Step ${stepNum}: text ---`)
break
case 'text-delta':
// Text streams in token-sized chunks. Use process.stdout.write so the
// chunks concatenate on the same line as a continuous string, rather
// than each chunk landing on its own line (which is what console.log
// would do because it appends a newline).
process.stdout.write(part.text)
break
case 'text-end':
process.stdout.write('\n')
break
case 'error':
console.error(`\n--- STREAM ERROR ---\n${part.error}`)
break
}
}
```
Each case corresponds to a distinct kind of progress event from the agent loop:
- `start-step` fires at the beginning of each agent turn (one round of model generation), so we use it to bump the visible step counter.
- `tool-call` and `tool-result` are the model's tool invocations and the values your tool's `execute` returned. Each is JSON-stringified for the log; very long inputs and outputs are truncated so the console stays readable.
- `text-start` / `text-delta` / `text-end` bracket every text emission, whether it is mid-run commentary or the final answer. The `delta` events are token-sized chunks, which is why we write them with `process.stdout.write` to keep them on one line.
- `tool-error` and `error` surface tool-execution failures and stream-level failures so a silent crash does not look like a stuck run.
#### Clean Up
When the script exits (whether normally or on error), the Daytona sandbox is deleted in a `finally` block. The downloaded `sieve_benchmark.png` and `findings.md` remain on your local disk. Those are the artifacts the agent produced for you to inspect after the run.
### 4. Switching Providers
The Vercel AI SDK is provider-agnostic. To swap Anthropic for OpenAI, install [`@ai-sdk/openai`](https://www.npmjs.com/package/@ai-sdk/openai) and change two lines:
```typescript
import { openai } from '@ai-sdk/openai'
// ...
const MODEL = openai('gpt-4o')
```
Set `OPENAI_API_KEY` in `.env` instead of `ANTHROPIC_API_KEY`. The default is Anthropic Claude Sonnet 4.6, but any AI SDK provider that supports tool calling works.
**Key advantages:**
- Secure, isolated execution: LLM-generated code, package installs, and shell commands never touch your machine.
- Multi-language in one sandbox: the same isolated environment runs Python, Node.js, and TypeScript. Switch languages per `runCode` call.
- Real artifact delivery: the agent's output is not just text. A real PNG chart and a markdown report land on your local disk.
- Reusable agent: the `ToolLoopAgent` instance can be invoked from a CLI, a streaming chat UI, or a background job without re-defining configuration.
# Build a Coding Agent Using Codex SDK and Daytona
import { Image } from 'astro:assets'
import codexSdkLunarLanderResult from '../../../../../assets/docs/images/codex-sdk-lunar-lander-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [OpenAI Codex](https://chatgpt.com/features/codex) inside a Daytona sandbox environment. The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and a Node.js agent is initialized inside it. The agent is based on the [Codex SDK](https://developers.openai.com/codex/sdk/).
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start
Creating sandbox...
Installing Codex agent in sandbox...
Press Ctrl+C at any time to exit.
User: create a 3d animated web-based, lunar lander game
Thinking...
🔨 ✓ Run: /bin/sh -lc ls
🔨 ✓ Run: /bin/sh -lc 'ls -a'
🔨 ✓ Run: /bin/sh -lc 'ls .daytona'
🔨 ✓ Run: /bin/sh -lc 'find /home/daytona -maxdepth 4 -name .git'
📝 Add /home/daytona/index.html
📝 Add /home/daytona/style.css
📝 Add /home/daytona/main.js
📝 Update /home/daytona/main.js
- Built a self-contained 3D lunar lander experience with HUD in index.html wired to main.js.
- Styled a glassy mission card, typography, and neon accents in style.css.
- Implemented the Three.js scene in main.js: starfield + noisy terrain with a flattened pad, modeled lander, thrust/fuel/rotation controls, gravity/drag physics, landing/crash checks, exhaust particles, and a chase camera. Controls: Space/↑ thrust, ←/→ yaw, W/S pitch, R restart.
Next steps:
1) Serve locally (e.g., cd /home/daytona && python3 -m http.server 8080) and open https://8080-e7c5deb5-7723-4bb8-93c6-25258d9b7c53.proxy.daytona.works.
2) Tune physics constants or terrain size if you want a harder/easier landing.
🗒️ To-do list:
- [x] Inspect workspace and set up project structure for web-based lunar lander game
- [x] Implement 3D scene, lunar lander controls, physics, and game loop
- [x] Add UI elements, polish, and quick sanity check (open file if feasible)
Usage Summary: Cached: 71936, Input: 103238, Output: 11311
User: start the server
Thinking...
🔨 ✓ Run: /bin/sh -lc 'cd /home/daytona && nohup python3 -m http.server 8080 --bind 0.0.0.0 >/home/daytona/server.log 2>&1 & echo $!'
Server started on port 8080 (pid 274). Open the game at:
https://8080-e7c5deb5-7723-4bb8-93c6-25258d9b7c53.proxy.daytona.works
If you need to stop it later: kill 274.
Usage Summary: Cached: 4096, Input: 22231, Output: 272
User:
Cleaning up...
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/openai/codex-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **OpenAI API key:** [OpenAI Developer Platform](https://platform.openai.com/api-keys)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_OPENAI_API_KEY=your_openai_key
```
:::caution[API Key Security]
Note: The `SANDBOX_OPENAI_API_KEY` key is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Agent's Architecture
This example consists of two main components:
- **Main Program:** The main program is a Node.js script (`src/index.ts`) that runs on your local machine. It uses the Daytona SDK to create and manage a Daytona sandbox. The main program provides a command line interface for interacting with the agent inside the sandbox.
- **Sandbox Agent:** The sandbox agent is a Node.js script (`agent/index.ts`) that runs inside the Daytona sandbox. It uses the Codex SDK to create a customized coding agent.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your OpenAI API key included in the environment variables.
2. Configures the Codex system prompt with Daytona-specific instructions and writes it to a `.codex/config.toml` file in the sandbox.
3. Uploads the agent package to the sandbox with [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
4. Installs the agent dependencies by running `npm install` in the uploaded agent directory.
5. Waits for user input and runs the agent asynchronously for each prompt.
#### Main Program Code
Custom system prompts for Codex must be configured via a `.codex/config.toml` file, so the main program creates this file in the sandbox before starting the agent:
```typescript
const systemPrompt = [
'You are running in a Daytona sandbox.',
'Use the /home/daytona directory instead of /workspace for file operations.',
`When running services on localhost, they will be accessible as: ${previewUrlPattern}`,
].join(' ')
const config = `developer_instructions = "${systemPrompt}"`
await sandbox.fs.createFolder('.codex', '755')
await sandbox.fs.uploadFile(Buffer.from(config, 'utf8'), '.codex/config.toml')
```
This prompt instructs the agent to use the correct file paths and preview link format for Daytona sandboxes.
After installing dependencies, the main program enters a loop to read user input and send it to the agent. For each user prompt it receives, it creates a new Daytona process session to run the agent command asynchronously and stream back the output:
```typescript
// Create a session to stream the agent output
const sessionId = `codex-session-${Date.now()}`
await sandbox.process.createSession(sessionId)
// Run the agent asynchronously, passing the prompt and OpenAI API key
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${environmentPrefix({ PROMPT: prompt })} npm exec --prefix /tmp/agent tsx -- /tmp/agent/index.ts`,
runAsync: true,
})
// Stream agent output as it arrives
if (!command.cmdId) throw new Error('Failed to start agent command in sandbox')
await sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId,
onStdout,
onStderr,
)
// Delete the session
await sandbox.process.deleteSession(sessionId)
```
The `onStdout` and `onStderr` callbacks are used to pass the agent's output back to the main program. After the agent finishes responding to the prompt, the main program waits for the next user input.
#### Sandbox Agent Code
The sandbox agent uses the [Codex SDK](https://developers.openai.com/codex/sdk/) to create a customized coding agent.
The agent is initialized with custom options that include the workspace directory:
```typescript
// Configure Codex options
const options: ThreadOptions = {
workingDirectory: '/home/daytona',
skipGitRepoCheck: true,
sandboxMode: 'danger-full-access',
}
```
The agent maintains thread state between requests by writing the thread ID to a file, allowing it to maintain context across multiple interactions:
```typescript
const threadIdPath = '/tmp/codex-thread-id'
const threadId = (await readFileIfExisting(threadIdPath))?.trim()
const thread: Thread = threadId
? codex.resumeThread(threadId, options)
: codex.startThread(options)
```
Additional code to stream agent responses follows the examples in OpenAI's [Codex SDK documentation](https://github.com/openai/codex/blob/main/sdk/typescript/README.md).
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Communicate with the agent directly in your terminal
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Thread persistence across multiple requests
- Simple setup and automatic cleanup
# OpenAI Agents SDK Guides
Guides for using the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) with Daytona.
# Using the OpenAI Agents SDK with Daytona Sandboxes
This guide walks through the core patterns for running AI agents in isolated cloud sandboxes using the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) and Daytona. We start from a simple example and progressively layer on multi-agent handoffs, memory, structured outputs, and human-in-the-loop workflows.
See also the [Text-to-SQL Agent with the OpenAI Agents SDK and Daytona](https://www.daytona.io/docs/en/guides/openai-agents/text-to-sql-agent-openai-agents-sdk.md) guide for a complete project built on these patterns.
---
## Prerequisites
Install the Agents SDK with the Daytona extra:
```shell
pip install openai-agents[daytona]
```
Set your environment variables:
```shell
export OPENAI_API_KEY=...
export DAYTONA_API_KEY=... # from https://app.daytona.io/dashboard/keys
```
## 1\. Give Your Agent a Shell
The basic pattern: declare a workspace, give an agent shell access, and let it explore, write code, and run it.
```py
from openai.types.responses import ResponseTextDeltaEvent
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.extensions.sandbox import DaytonaSandboxClient, DaytonaSandboxClientOptions
DAYTONA_ROOT = "/home/daytona/workspace"
# Declare workspace contents declaratively
# Use Daytona's home directory as root instead of the default /workspace.
manifest = Manifest(root=DAYTONA_ROOT, entries={
"data/sales.csv": File(content=b"quarter,revenue\nQ1,3200000\nQ2,3600000\nQ3,4200000\nQ4,3900000"),
"requirements.txt": File(content=b"pandas\nmatplotlib"),
})
agent = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions=(
"You're a data analyst with shell access to a sandbox. "
"Inspect the workspace, install dependencies, write and run code to answer questions."
),
default_manifest=manifest,
capabilities=[Shell()],
)
client = DaytonaSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())
)
result = Runner.run_streamed(
agent,
"Which quarter had the highest revenue? Write a script to plot the trend and save it as chart.png.",
run_config=run_config,
)
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)
elif event.type == "run_item_stream_event":
if event.name == "tool_called":
raw = event.item.raw_item
name = raw.get("name", "") if isinstance(raw, dict) else getattr(raw, "name", "")
args = raw.get("arguments", "") if isinstance(raw, dict) else getattr(raw, "arguments", "")
print(f"\n[{name}] {args}")
elif event.name == "tool_output":
print(f" → {event.item.output[:200]}")
await client.close()
```
The agent will likely `cat` the CSV, `pip install -r requirements.txt`, write a Python script, run it, and report back, all through the shell tool. A typical run might look like:
```
[exec_command] {"cmd": "cat data/sales.csv"}
→ quarter,revenue\nQ1,3200000\nQ2,3600000\nQ3,4200000\nQ4,3900000
[exec_command] {"cmd": "pip install -r requirements.txt"}
→ Successfully installed pandas matplotlib ...
[exec_command] {"cmd": "python plot.py"}
→ Chart saved to chart.png
Q3 had the highest revenue at $4.2M. I've saved a trend chart to chart.png.
```
**What's happening:**
- **`Manifest`** describes the workspace declaratively: files, directories, and environment variables (via `environment=Environment(value={"API_KEY": "..."})`, where `Environment` is imported from `agents.sandbox.manifest`). You can also pass `Manifest(entries={})` for an empty workspace and let the agent create everything from scratch.
- **`SandboxAgent`** adds `default_manifest` and `capabilities` on top of a regular `Agent`. You can still pass `tools=` (function tools) and `mcp_servers=` alongside capabilities.
- **`Shell`** gives the model an `exec_command` tool that can run `cat`, `ls`, `find`, `grep`, `pip install`, `python script.py`, etc. inside the sandbox. The agent can read *and* write: creating files, installing packages, and running programs are all fair game.
- **`DaytonaSandboxClient`** provisions a remote cloud sandbox.
- **`Runner.run_streamed`** streams text token-by-token and emits structured events when tools are called.
The sandbox is fully isolated, so there's no risk to your host machine. The agent has full Linux access inside it.
## 2\. Multi-Turn Conversations
The previous example runs a single question and exits. In practice you'll often want an interactive session where the human asks questions, the agent responds, and conversation history carries forward. The sandbox stays alive across turns so the agent can build on previous work.
```py
client = DaytonaSandboxClient()
session = await client.create(manifest=manifest, options=DaytonaSandboxClientOptions())
await session.start()
run_config = RunConfig(sandbox=SandboxRunConfig(session=session))
conversation = []
while True:
question = input("> ")
if question.strip().lower() == "exit":
break
input_items = conversation + [{"role": "user", "content": question}]
result = Runner.run_streamed(agent, input_items, run_config=run_config)
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)
print()
# Carry conversation history forward so the agent remembers previous turns
conversation = result.to_input_list()
await session.aclose()
await client.close()
```
This example uses `result.to_input_list()`, which serializes the full conversation (including tool calls and their results) into a format you can pass back on the next turn. The agent sees the entire history, so follow-ups like "break that down by quarter" or "now plot it" just work. The SDK also supports other state strategies: sessions, `conversation_id`, and `previous_response_id` – see the [State and conversation management](https://openai.github.io/openai-agents-python/running_agents/#state-and-conversation-management) docs for the full picture.
This pattern composes with everything else in this guide. You can add handoffs, memory, pause/resume, etc. on top of a multi-turn loop.
## 3\. Pause and Resume
By default, when a session shuts down the sandbox is deleted. Setting `pause_on_exit=True` changes this: on shutdown, the SDK calls Daytona's pause API (`sandbox.stop()`) instead of `sandbox.delete()`. The sandbox stays on Daytona's infrastructure in a paused state, preserving the filesystem (including any installed packages).
To reconnect on the next run, you need two things:
1. **Daytona keeps the sandbox alive**, paused on their side, identifiable by its sandbox ID.
2. **Your code remembers the sandbox ID**. The SDK captures this in `DaytonaSandboxSessionState`, a Pydantic model you serialize to disk.
When you call `client.resume(saved_state)`, the SDK uses the `sandbox_id` from that state to call `daytona.get(sandbox_id)`. If the sandbox is still there, it calls `sandbox.start()` to wake it. The workspace is already populated, so it skips full manifest apply but still reapplies ephemeral state (like environment variables) and restores snapshots if needed. If the sandbox has expired or been deleted, `resume()` falls through and creates a fresh one from the same config.
```py
from pathlib import Path
from agents.extensions.sandbox import (
DaytonaSandboxClient,
DaytonaSandboxClientOptions,
DaytonaSandboxSessionState,
)
STATE_FILE = Path(".session_state.json")
client = DaytonaSandboxClient()
options = DaytonaSandboxClientOptions(pause_on_exit=True)
# Try to resume a previously paused sandbox
session = None
if STATE_FILE.exists():
saved = DaytonaSandboxSessionState.model_validate_json(STATE_FILE.read_text())
old_sandbox_id = saved.sandbox_id # snapshot before resume() mutates it
try:
session = await client.resume(saved)
if session.state.sandbox_id == old_sandbox_id:
print("Reconnected to existing sandbox.")
else:
print("Previous sandbox expired. Created a new one.")
except Exception:
session = None # fall through to fresh creation
if session is None:
session = await client.create(manifest=manifest, options=options)
# Save state immediately so crashes don't orphan the sandbox
STATE_FILE.write_text(session.state.model_dump_json(indent=2))
# ... run your agent ...
# On clean exit: aclose() persists the workspace, then pauses (or deletes) the remote sandbox
await session.aclose()
await client.close()
```
The Agents SDK also has its own **workspace persistence** mechanism (`persist_workspace`/`hydrate_workspace`) that tars up workspace files and saves them externally (local disk, S3). This is useful when the sandbox itself is gone and you need to restore contents into a new one. It's distinct from **Daytona snapshots** (`sandbox_snapshot_name`), which are pre-built sandbox templates you create sandboxes *from*.
## 4\. Handoffs: Routing Work Between Agents
A `SandboxAgent` can hand off to a regular `Agent` and vice versa. Not every agent needs sandbox access: a copywriter can draft an email without a shell.
```py
from agents import Agent, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.extensions.sandbox import DaytonaSandboxClient, DaytonaSandboxClientOptions
manifest = Manifest(root="/home/daytona/workspace", entries={
"data/sales.csv": File(content=b"quarter,region,revenue\nQ1,NA,3200000\nQ1,EU,2100000\n..."),
})
# The copywriter receives the analyst's findings (no sandbox needed)
copywriter = Agent(
name="Client Email Drafter",
model="gpt-5.4",
instructions="Turn the analyst's findings into a short, friendly client-facing email.",
)
# The analyst has shell access to crunch data, then hands off to the copywriter
analyst = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions=(
"Analyze the sales data in the workspace. Write and run code to compute trends. "
"Then hand off your findings to the Client Email Drafter."
),
default_manifest=manifest,
capabilities=[Shell()],
handoffs=[copywriter],
)
client = DaytonaSandboxClient()
result = await Runner.run(
analyst,
"Summarize Q1 performance by region for the client.",
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
)
await client.close()
print(result.final_output) # a polished email, written by the copywriter
```
**The flow:** Analyst (sandbox, reads CSV, runs a script) → Copywriter (no sandbox, writes the email). The final output comes from the copywriter, but it's grounded in the analyst's computed results.
Handoffs can also be **circular**: agents pass control back and forth until one decides to respond directly instead of handing off, which ends the run. In the example above, that would look like:
```py
from agents import handoff
copywriter.handoffs = [handoff(analyst)]
analyst.handoffs = [handoff(copywriter)]
```
You can also have multiple sandbox agents, each with their own isolated workspace and separate `RunConfig`, as shown in the next section.
## 5\. Sandbox Agents as Tools
Instead of handoffs (sequential), you can run sandbox agents as parallel tools under an orchestrator:
```py
import json
from pydantic import BaseModel
class PricingReview(BaseModel):
risk: str
summary: str
class RolloutReview(BaseModel):
risk: str
blockers: list[str]
# By default, Pydantic output_type results are stringified (repr) when passed back
# as tool output. This extractor ensures the orchestrator receives clean JSON instead.
async def structured_output_extractor(result) -> str:
final_output = result.final_output
if isinstance(final_output, BaseModel):
return json.dumps(final_output.model_dump(mode="json"), sort_keys=True)
return str(final_output)
# Each reviewer gets its own isolated workspace
pricing_agent = SandboxAgent(
name="Pricing Reviewer",
default_manifest=pricing_docs_manifest,
capabilities=[Shell()],
output_type=PricingReview,
...
)
rollout_agent = SandboxAgent(
name="Rollout Reviewer",
default_manifest=rollout_docs_manifest,
capabilities=[Shell()],
output_type=RolloutReview,
...
)
# Orchestrator calls them like tools, each in its own sandbox
client = DaytonaSandboxClient()
orchestrator = Agent(
name="Deal Desk Coordinator",
instructions="Use both review tools, then synthesize a recommendation.",
tools=[
pricing_agent.as_tool(
tool_name="review_pricing",
tool_description="Review the pricing packet.",
custom_output_extractor=structured_output_extractor,
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
),
rollout_agent.as_tool(
tool_name="review_rollout",
tool_description="Review the rollout plan.",
custom_output_extractor=structured_output_extractor,
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
),
],
)
result = await Runner.run(orchestrator, "Review the Acme Corp renewal deal.")
print(result.final_output)
await client.close()
```
Each sandbox agent runs in its own isolated environment. The orchestrator never sees the files; it only gets the structured output as JSON via the `custom_output_extractor`. This is great for **fan-out** patterns where you need multiple independent analyses.
## 6\. Memory Across Sessions
The `Memory` capability lets an agent learn from previous runs. It extracts durable facts and preferences from each conversation, consolidates them into structured files in the workspace, and automatically injects a summary into the agent's instructions on future runs.
```py
from agents.sandbox import LocalSnapshotSpec, SandboxRunConfig
from agents.sandbox.capabilities import ApplyPatch, Memory, Shell
agent = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions="Analyze the workspace and answer questions.",
default_manifest=manifest,
capabilities=[
Shell(),
ApplyPatch(),
Memory(),
],
)
snapshot = LocalSnapshotSpec(base_path=Path("/tmp/my-agent-snapshots"))
# First run: agent learns user preferences.
# Memory artifacts are written to the workspace when the session closes.
session = await client.create(manifest=manifest, snapshot=snapshot)
async with session:
run_config = RunConfig(sandbox=SandboxRunConfig(session=session))
result1 = await Runner.run(agent, "Fix the bug. I prefer minimal patches.", run_config=run_config)
# Second run: resume the workspace so the agent sees the memory files from run 1.
resumed = await client.resume(session.state)
async with resumed:
run_config = RunConfig(sandbox=SandboxRunConfig(session=resumed))
result2 = await Runner.run(agent, "Add a test for the fix.", run_config=run_config)
```
Memory consolidation runs as a background task and flushes when the session closes, so the close/resume cycle ensures run 2 sees the artifacts from run 1\. You can also keep a single sandbox session open across runs (like section 2), though memory visibility then depends on whether the background task has finished.
`Memory()` with no arguments enables both reading and writing with live updates (the agent can repair stale memory in place). It requires `Shell` and `ApplyPatch` as sibling capabilities. You can tune the behavior:
```py
from agents.sandbox.config import MemoryReadConfig, MemoryWriteConfig
# Write-only (no auto-injection of memory into instructions):
Memory(read=None)
# Read-only (no background memory generation):
Memory(write=None)
# Custom write settings:
Memory(write=MemoryWriteConfig(
batch_size=2,
extra_prompt="Pay attention to which SQL patterns work best for this dataset.",
))
# Disable live updates (agent reads memory but won't repair stale entries):
Memory(read=MemoryReadConfig(live_update=False))
```
**How it works under the hood:**
After each `Runner.run()` completes, the SDK serializes the run (user input, tool calls, outputs, and final response, filtering out system/developer items and reasoning) into a JSONL file in `rollouts/`. A background pipeline then processes these in two phases:
1. **Phase 1 (per-rollout extraction):** A lightweight model (`gpt-5.4-mini`) reads each rollout transcript and extracts durable facts and preferences into `memory/raw_memories/` and `memory/rollout_summaries/`.
2. **Phase 2 (consolidation):** Once enough phase-1 results accumulate (controlled by `batch_size`), a stronger model (`gpt-5.4`) consolidates everything into `memory/MEMORY.md` (a structured, grep-friendly handbook) and `memory/memory_summary.md` (a compact index). A final phase-2 pass always runs on session shutdown.
Both phases run in a background `asyncio.Task`, so they don't block the agent's main work.
On subsequent runs, the `Memory` capability reads `memory/memory_summary.md` from the workspace and injects it into the agent's instructions (truncated to 15k tokens). The agent also gets guidance on when to grep `memory/MEMORY.md` for deeper context. This injection happens automatically — you don't need to wire it up yourself.
The full set of generated artifacts:
- `rollouts/`: JSONL rollout files (raw transcripts of each run)
- `memory/MEMORY.md`: detailed, grep-friendly handbook
- `memory/memory_summary.md`: compact summary, auto-injected into instructions
- `memory/raw_memories/`: individual learned facts (one file per rollout)
- `memory/raw_memories.md`: concatenated version of the above, fed into phase 2
- `memory/rollout_summaries/`: per-rollout summaries
- `memory/skills/`: optional reusable procedures the consolidation model may create
If you combine this with pause/resume (\#3), the memory files survive across sessions. The workspace persistence model includes all runtime-created files by default (only `ephemeral=True` manifest entries are excluded). So on the next run, the agent starts with full context from previous sessions — no extra wiring needed.
## 7\. Custom Capabilities
Capabilities are plugins that inject tools and instructions into a sandbox agent. The built-in ones (`Shell`, `ApplyPatch`, `Vision`) cover common cases, but you can write your own:
```py
from agents.sandbox.capabilities.capability import Capability
from agents.tool import Tool, function_tool
class ExposePort(Capability):
type: str = "expose_port"
def tools(self) -> list[Tool]:
session = self.session # bound automatically by the framework
@function_tool
async def get_app_url(port: int) -> str:
"""Get the public URL for a port running in this sandbox."""
endpoint = await session.resolve_exposed_port(port)
return endpoint.url_for("http")
return [get_app_url]
```
**Note:** `resolve_exposed_port` requires the port to be predeclared in the client options, e.g. `DaytonaSandboxClientOptions(exposed_ports=(8080,))`. Without this, the call raises `ExposedPortUnavailableError`.
Use this to expose domain-specific operations (database queries, API testing, cloud storage access) as tools the agent can call.
## Quick Reference: DaytonaSandboxClientOptions
| Option | Default | Description |
| :---- | :---- | :---- |
| `image` | `None` | OCI-compliant image to boot from |
| `env_vars` | `None` | Environment variables injected at creation |
| `exposed_ports` | `()` | Ports accessible via signed preview URLs |
| `pause_on_exit` | `False` | Pause sandbox instead of deleting on cleanup |
| `auto_stop_interval` | `0` | Seconds of inactivity before auto-pause (0 \= disabled) |
| `create_timeout` | `60` | Timeout in seconds for sandbox creation |
| `resources` | `None` | CPU/memory/disk configuration |
## Patterns at a Glance
| Pattern | When to Use | Key Concept |
| :---- | :---- | :---- |
| **Give Your Agent a Shell** (\#1) | Agent needs to read, write, or run code | `Manifest` \+ `Shell` |
| **Multi-Turn Conversations** (\#2) | Interactive sessions with a human | `result.to_input_list()` |
| **Pause/Resume** (\#3) | Long-running or iterative tasks | `pause_on_exit` \+ `client.resume(state)` |
| **Handoffs** (\#4) | Pipeline: analyze → write → review | `handoffs=[next_agent]` |
| **Agents as Tools** (\#5) | Parallel independent analyses | `agent.as_tool(run_config=...)` |
| **Memory** (\#6) | Preferences that persist across sessions | `Memory()` \+ `MemoryReadConfig`/`MemoryWriteConfig` |
| **Custom Capabilities** (\#7) | Domain-specific sandbox operations | Subclass `Capability` |
## What's Next
For a complete project that puts these patterns to work, see [**Building a Text-to-SQL Agent with OpenAI Agents SDK and Daytona**](https://www.daytona.io/docs/en/guides/openai-agents/text-to-sql-agent-openai-agents-sdk.md), a conversational agent that queries real federal spending data, combining multi-turn conversations, pause/resume, memory, and preview URLs.
# Text-to-SQL Agent with the OpenAI Agents SDK and Daytona
In this guide we use the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) and Daytona sandboxes to build a conversational agent that answers natural-language questions about NASA's federal spending. It translates questions into SQL, runs them against a SQLite database of real USAspending.gov data (FY2021-FY2025), and explains the results. The agent runs inside a Daytona sandbox, which provides isolated code execution, pause/resume across runs (so you can reconnect to the same sandbox without re-downloading data), cross-session memory (so the agent learns from previous conversations), and signed preview URLs for downloading query results.
---
### What a session looks like
```
> How much did NASA spend in FY2024?
[SQL] (limit 10)
SELECT SUM(federal_action_obligation) AS total_obligations
FROM spending
WHERE fiscal_year = 2024;
→ Result (1 rows)
| total_obligations |
|-------------------|
| 21352116106.41 |
↓ https://8080-your-sandbox-id.proxy.daytona.works/query_1775081861_281.csv
NASA obligated **$21.35 billion** in **FY2024**.
> Who are the top 5 recipients?
[SQL] (limit 10)
SELECT COALESCE(NULLIF(recipient_parent_name, ''), NULLIF(recipient_name, ''), 'UNKNOWN') AS recipient_entity,
SUM(federal_action_obligation) AS total_obligations
FROM spending
WHERE fiscal_year = 2024
GROUP BY recipient_entity
ORDER BY total_obligations DESC
LIMIT 5;
→ Result (5 rows)
| recipient_entity | total_obligations |
|--------------------------------------|-------------------|
| CALIFORNIA INSTITUTE OF TECHNOLOGY | 2247310017.56 |
| SPACE EXPLORATION TECHNOLOGIES CORP. | 1996992060.56 |
| THE BOEING COMPANY | 1540996029.73 |
| LOCKHEED MARTIN CORP | 1208072461.33 |
| NORTHROP GRUMMAN CORPORATION | 641375071.34 |
↓ https://8080-your-sandbox-id.proxy.daytona.works/query_1775081919_307.csv
In FY2024, the top 5 recipients were **California Institute of Technology ($2.25B)**,
**SpaceX ($2.00B)**, **Boeing ($1.54B)**, **Lockheed Martin ($1.21B)**,
and **Northrop Grumman ($641.38M)**.
```
The agent keeps conversation context across turns, so one can naturally follow-up on previous questions. Each result also gets a download link via the Daytona sandbox's exposed port.
## 1\. Setup
Set your environment variables:
```shell
export OPENAI_API_KEY=...
export DAYTONA_API_KEY=... # from https://app.daytona.io/dashboard/keys
```
Clone the repo, install dependencies and run:
```shell
git clone https://github.com/openai/openai-agents-python
cd openai-agents-python
uv sync --extra daytona
uv run python -m examples.sandbox.extensions.daytona.usaspending_text2sql.agent
```
On the first run, the agent fetches NASA spending data from the USAspending.gov API and builds a SQLite database inside the sandbox. This takes a few minutes. Subsequent runs reuse the paused sandbox and skip this step entirely.
## 2\. The Workspace
The manifest declares what goes into the sandbox:
```py
from agents.sandbox import Manifest
from agents.sandbox.entries import Dir, LocalDir, LocalFile
manifest = Manifest(
root=WORKSPACE_ROOT,
entries={
"setup_db.py": LocalFile(src=SETUP_DB_PATH),
"schema": LocalDir(src=SCHEMA_DIR),
"data": Dir(ephemeral=True),
},
)
```
The main entries:
- **`setup_db.py`** fetches NASA spending data from the USAspending.gov bulk download API and builds a SQLite database. It downloads contracts, grants, and IDVs for FY2021-FY2025, parses the CSVs, and creates an indexed `spending` table with \~30 columns covering amounts, recipients, locations, industry codes, and more. It's idempotent: if the DB already exists, it skips the download.
- **`schema/`** contains documentation the agent uses to understand the data:
- `overview.md` has the table schema, column descriptions, and example SQL patterns. This gets injected directly into the agent's `developer_instructions`.
- `tables/spending.md` has detailed per-column docs the agent can read via shell if needed.
- `glossary.md` has official USAspending terminology (what "obligation" vs "outlay" means, etc.), fetched from the USAspending API during setup.
- **`data/`** is where the SQLite DB lands. It's marked `ephemeral=True` so it's excluded from workspace snapshots (the DB can always be rebuilt from the API).
## 3\. The SqlCapability
The core of this example is a custom `Capability` that gives the agent a guardrailed `run_sql` tool. The agent could run `sqlite3` directly via the shell, but that would give it unrestricted access. The capability enforces safety at multiple levels:
```py
class SqlCapability(Capability):
type: Literal["sql"] = "sql"
db_path: str = "data/usaspending.db"
max_display_rows: int = 100
max_csv_rows: int = 10_000
timeout_seconds: float = 30.0
results_dir: str = "results"
```
**Guardrails (defense in depth):**
- **Read-only access**: SQLite opened with `?mode=ro` URI \+ `PRAGMA query_only = ON`
- **Statement validation**: Only `SELECT`, `WITH`, `EXPLAIN`, `PRAGMA` are allowed
- **Row limits**: 100 rows displayed to the model, up to 10,000 saved as downloadable CSV
- **Timeouts**: Queries killed after 30 seconds
The tool returns structured JSON (columns, rows, row counts, CSV filename), and the capability injects instructions telling the agent to prefer aggregations, use the schema docs, and explain its query logic.
## 4\. The Agent
The pieces come together in a `SandboxAgent` with four capabilities:
```py
from agents.sandbox import SandboxAgent
from agents.sandbox.capabilities.compaction import Compaction
from agents.sandbox.capabilities.memory import Memory
from agents.sandbox.capabilities.shell import Shell
from agents.sandbox.config import MemoryReadConfig, MemoryWriteConfig
agent = SandboxAgent(
name="NASA Spending Q&A",
default_manifest=manifest,
model="gpt-5.4",
instructions=(
"You are a helpful data analyst that answers questions about NASA federal spending "
"by writing and executing SQL queries."
),
developer_instructions=DEVELOPER_INSTRUCTIONS,
capabilities=[
SqlCapability(db_path="data/usaspending.db"),
Shell(),
Compaction(),
Memory(
read=MemoryReadConfig(live_update=False),
write=MemoryWriteConfig(batch_size=5),
),
],
)
```
- **`SqlCapability`** provides the guardrailed `run_sql` tool (section 3).
- **`Shell()`** gives the agent general shell access, mainly so it can `cat schema/tables/spending.md` or `cat schema/glossary.md` when it needs column details or term definitions beyond what's in the overview.
- **`Compaction()`** handles long sessions. After the conversation grows past a token threshold, the SDK automatically compacts earlier turns to keep context manageable. Without this, a long Q\&A session would eventually hit the model's context limit.
- **`Memory()`** gives the agent cross-session recall. The `Memory` capability is covered in detail in section 8\. The key config choices here: `live_update=False` means the agent isn't instructed to edit memory files mid-conversation (and doesn't need `ApplyPatch`), and `batch_size=5` batches the expensive consolidation step to run every 5 turns instead of every turn.
The `developer_instructions` load the full schema overview from `schema/overview.md` plus guidelines about data caveats (obligations vs outlays, masked recipients, etc.), so the agent starts every conversation already knowing the schema and quirks of the data.
## 5\. The Conversation Loop
Each turn appends the user's question to the conversation history, streams the agent's response (printing text deltas, tool calls, and formatted results), then carries the full history forward via `result.to_input_list()`. This is what lets follow-ups like "break that down by year" work, because the history includes tool calls and their results, so the agent knows what "that" refers to.
```py
input_items = conversation + [{"role": "user", "content": question}]
result = Runner.run_streamed(agent, input_items, run_config=run_config)
async for event in result.stream_events():
# print text deltas, tool calls, and tool outputs as they arrive
...
conversation = result.to_input_list()
```
The actual example includes SQL syntax highlighting and table formatting for the terminal output. See `agent.py` for the full streaming loop.
## 6\. Pause and Resume
The first run is slow because `setup_db.py` needs to download data from the USAspending API. With `pause_on_exit=True`, the sandbox pauses instead of being deleted, so subsequent runs reconnect to the existing sandbox with the database already built.
```py
# Core pattern (see agent.py for full error handling):
client = DaytonaSandboxClient()
options = DaytonaSandboxClientOptions(pause_on_exit=True, exposed_ports=(8080,))
saved_state = _load_session_state() # returns None on first run
if saved_state is not None:
session = await client.resume(saved_state)
else:
session = await client.create(manifest=agent.default_manifest, options=options)
_save_session_state(session.state)
```
The session state is serialized to a JSON file. On the next run, `client.resume()` uses the saved `sandbox_id` to find and wake the paused sandbox. If it's expired or been deleted, it falls through and creates a fresh one. Type `exit` to pause the sandbox, or `destroy` to delete it.
## 7\. Downloadable Results via Exposed Ports
Each query result is saved as a CSV inside the sandbox. To make these downloadable, a simple HTTP file server runs on port 8080:
```py
# Start a file server inside the sandbox
await session.exec("mkdir -p results", timeout=5.0)
await session.exec(
"nohup python3 -m http.server 8080 --directory results > /dev/null 2>&1 &",
timeout=5.0,
)
# Get the signed public URL
endpoint = await session.resolve_exposed_port(8080)
downloads_url = endpoint.url_for("http")
# -> https://8080-abc123.proxy.daytona.works
```
The `run_sql` tool includes the CSV filename in its response, and the agent surfaces the download link. You can see this in the session example at the top: each result has a `↓` download URL (signed Daytona preview link).
## 8\. Memory Across Sessions
Within a single REPL session, the agent has the full conversation history and remembers every question and answer. But when you type `exit` and come back later, that history is gone. The `Memory` capability bridges this gap by extracting durable learnings from each session and making them available to future sessions.
**How it works:**
After each conversational turn, the SDK serializes the full exchange (input, tool calls, outputs, response) into a rollout file. A background pipeline then processes these:
1. **Phase 1** (after every turn): A lightweight model (`gpt-5.4-mini`) reads the rollout and extracts durable facts (useful query patterns, data caveats, column quirks, user preferences) into `memory/raw_memories/` and `memory/rollout_summaries/`.
2. **Phase 2** (every 5 turns, or on shutdown): A stronger model (`gpt-5.4`) consolidates everything into `memory/MEMORY.md` (a detailed, grep-friendly handbook) and `memory/memory_summary.md` (a compact summary).
Both phases run in background `asyncio` tasks, so they don't block the conversation.
**How it persists:**
Memory files live in the sandbox filesystem and survive as long as the paused sandbox does. When you reconnect, the `Memory` capability reads them back. If the sandbox is destroyed or can't be reconnected, memory is lost and the agent starts fresh.
## Quick Reference
| Component | What it does |
| :---- | :---- |
| `setup_db.py` | Fetches NASA data from USAspending API, builds SQLite DB |
| `schema/overview.md` | Table schema and example queries, injected into agent instructions |
| `schema/glossary.md` | Official USAspending term definitions |
| `SqlCapability` | Guardrailed `run_sql` tool (read-only, row limits, timeouts) |
| `Shell()` | General shell access for reading schema docs and memory |
| `Compaction()` | Automatic context compression for long sessions |
| `Memory()` | Cross-session learning: extracts and recalls durable facts |
| `pause_on_exit=True` | Sandbox persists across runs (avoids re-downloading data) |
# Build a Coding Agent Using Amp Code and Daytona
import { Image } from 'astro:assets'
import ampSdkCodingAgentDemo from '../../../../../assets/docs/images/amp-sdk-coding-agent.gif'
This guide demonstrates how to run an autonomous coding agent based on [Amp Code](https://ampcode.com/) inside a Daytona sandbox environment. The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and the Amp CLI is installed inside it. The agent uses Amp's [streaming JSON mode](https://ampcode.com/manual#cli-streaming-json) for programmatic control.
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start
Creating sandbox...
Installing Amp CLI...
Starting Amp Code...
Thinking...
Got it! I'm ready to help. What would you like to build or work on?
Agent ready. Press Ctrl+C at any time to exit.
User: Create a Kanji flashcard app
Thinking...
> I'll create a Kanji flashcard app with flip animations, progress tracking, and multiple study modes. Here's the preview URL:
https://8000-29baaaf7-767a-4dff-8129-1e6ec2100b3e.daytonaproxy01.net
🔧 create_file /home/daytona/index.html
Successfully created file /home/daytona/index.html
🔧 create_file /home/daytona/start.sh
Running `python3 -m http.server 8000` via session command...
User:
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/amp/amp-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Amp API key:** [Amp Settings](https://ampcode.com/settings)
:::caution[Amp Paid Credits Required]
Amp's execute mode (`-x`) requires paid credits and cannot use the free tier. [Add credits here](https://ampcode.com/pay) before running this example.
:::
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_AMP_API_KEY=your_amp_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Script
This example uses Amp's `--stream-json` mode for streaming output and the `-x` (execute) flag for autonomous operation. Commands are sent via a PTY (pseudo-terminal) for real-time streaming.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with the Amp API key.
2. Installs the Amp CLI globally in the sandbox.
3. Creates a PTY for streaming output from Amp.
4. Sends a Daytona-aware system prompt as the first user message (preview URL pattern + instruction to write server startup command to `/home/daytona/start.sh`).
5. Enters the readline loop to send prompts and receive streamed responses.
6. On Ctrl+C, kills the PTY session, deletes the sandbox, and exits.
#### PTY Communication
The agent uses a pseudo-terminal (PTY) for streaming output from Amp:
```ts
// Create a PTY for streaming output from Amp
this.ptyHandle = await this.sandbox.process.createPty({
id: `amp-pty-${Date.now()}`,
cols: 120,
rows: 30,
onData: (data: Uint8Array) => this.handleData(data),
})
// Wait for PTY connection
await this.ptyHandle.waitForConnection()
```
#### Running Amp Commands
Each prompt is sent as an `amp` command with the `-x` (execute) flag for autonomous operation. The agent uses Amp's thread system to maintain conversation context:
```ts
// Run an amp command via PTY and wait for completion
private async runAmpCommand(args: string[]): Promise {
const command = ['amp', '--dangerously-allow-all', '--stream-json', '-m smart', ...args].join(' ')
// Send command to the PTY
await this.ptyHandle.sendInput(`cd /home/daytona && ${command}\n`)
// Wait for the response to complete (signaled by result message)
await new Promise((resolve) => {
this.onResponseComplete = resolve
})
}
// Process a user prompt
async processPrompt(prompt: string): Promise {
if (this.threadId) {
// Continue existing thread
await this.runAmpCommand(['-x', JSON.stringify(prompt), 'threads', 'continue', this.threadId])
} else {
// Start new thread
await this.runAmpCommand(['-x', JSON.stringify(prompt)])
}
}
```
#### Streaming JSON Messages
Amp outputs JSON lines that can be parsed to track agent activity. The `handleData` method buffers incoming data and processes complete lines:
```ts
// Handle streamed data from PTY
private handleData(data: Uint8Array): void {
// Append new data to the buffer
this.buffer += new TextDecoder().decode(data)
// Split the buffer into complete lines
const lines = this.buffer.split('\n')
// Keep any incomplete line in the buffer for next time
this.buffer = lines.pop() || ''
// Process each complete line
for (const line of lines.filter((l) => l.trim())) {
this.handleJsonLine(line)
}
}
```
Message types from Amp's streaming JSON:
- **system**: Session initialization with `subtype: 'init'` and `session_id` for thread tracking
- **assistant**: AI responses with text content and tool usage blocks
- **user**: Tool results (output from executed tools)
- **result**: Final execution result (success or error) - signals response completion
```ts
private handleJsonLine(line: string): void {
const parsed = JSON.parse(line) as AmpMessage
if (parsed.type === 'system' && parsed.subtype === 'init') {
// Capture thread ID for conversation continuation
const sysMsg = parsed as { session_id?: string }
if (sysMsg.session_id) this.threadId = sysMsg.session_id
} else if (parsed.type === 'assistant') {
// Display text and tool_use blocks
const msg = parsed as AssistantMessage
for (const block of msg.message.content) {
if (block.type === 'text') { /* render text */ }
else if (block.type === 'tool_use') { /* display tool */ }
}
} else if (parsed.type === 'user') {
// Tool results: display output
} else if (parsed.type === 'result') {
// Signal response completion
this.onResponseComplete?.()
}
}
```
#### System Prompt and Main Loop
A Daytona-aware system prompt is sent as the first user message. It instructs the agent to use the preview URL pattern and to write the server start command into `/home/daytona/start.sh` (instead of executing directly in Amp), then provide the preview URL:
```ts
const defaultSystemPrompt = [
'You are running in a Daytona sandbox.',
`When running services on localhost, they will be accessible as: ${previewUrlPattern}`,
'When you need to start a server, DO NOT run it directly.',
'Instead, write only the server start command to /home/daytona/start.sh (one command, no markdown).',
'After writing the start command, provide the preview URL to the user.',
].join(' ')
const ampSession = new AmpSession(sandbox)
await ampSession.initialize({ systemPrompt: defaultSystemPrompt })
```
When Amp is ready, the script runs a readline loop:
```ts
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
while (true) {
const prompt = await new Promise((resolve) => rl.question('User: ', resolve))
if (prompt.trim()) {
await ampSession.processPrompt(prompt)
await startServerFromScript()
}
}
```
The readline loop waits for user input, sends it to the agent, and displays the streamed response. If Amp produced `/home/daytona/start.sh`, the script is then launched via Daytona's session command API so long-running/background server startup does not hang Amp turns.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Streaming JSON output for real-time tool activity feedback
- PTY-based communication for streaming output
- Thread-based conversation continuity across prompts
- Uses Amp's `smart` mode for state-of-the-art model capabilities
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Automatic cleanup on exit
# Generate Verified Code With Google ADK Agent
This guide demonstrates how to use the `DaytonaPlugin` for Google ADK to build an agent that generates, tests, and verifies code in a secure sandbox environment. The plugin enables agents to execute Python, JavaScript, and TypeScript code, run shell commands, and manage files within isolated Daytona sandboxes.
In this example, we build a code generator agent that takes a natural language description of a function, generates the implementation in TypeScript, creates test cases, executes them in the sandbox, and iterates until all tests pass before returning the verified code.
---
### 1. Workflow Overview
You describe the function you want in plain English, specifying the language (Python, JavaScript, or TypeScript). The agent generates the implementation, writes tests for it, and executes everything in a Daytona sandbox. If tests fail, the agent automatically fixes the code and re-runs until all tests pass. Only then does it return the verified, working code.
The key benefit: you receive code that has already been tested and verified, not just generated.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona repository and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona
cd daytona/guides/python/google-adk/code-generator-agent/gemini
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher**. It's recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install -U google-adk daytona-adk python-dotenv
```
The packages include:
- `google-adk`: Google's Agent Development Kit for building AI agents
- `daytona-adk`: Provides the `DaytonaPlugin` that enables secure code execution in Daytona sandboxes
- `python-dotenv`: Used for loading environment variables from `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Google API key:** Get it from [Google AI Studio](https://aistudio.google.com/apikey)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
GOOGLE_API_KEY=***
```
### 3. Understanding the Core Components
Before diving into the implementation, let's understand the key components we'll use:
#### Google ADK Components
- **Agent**: The AI model wrapper that processes requests and decides which tools to use. It receives instructions, has access to tools, and generates responses.
- **App**: A top-level container that bundles agents with plugins into a single configuration unit. It provides centralized management for shared resources and defines the root agent for your workflow.
- **InMemoryRunner**: The execution engine that runs agents and manages conversation state. It orchestrates the event-driven execution loop, handles message processing, and manages services like session history and artifact storage.
:::note[Running the Agent]
There are two ways to run Google ADK agents: using the `App` class with `InMemoryRunner`, or using `InMemoryRunner` directly with just an agent. The `App` serves as a configuration container that bundles agents with plugins, while the `Runner` handles actual execution and lifecycle management. This guide uses the `App` approach for cleaner organization of agents and plugins.
:::
#### Daytona Plugin
The `DaytonaPlugin` provides tools that allow the agent to:
- Execute code in Python, JavaScript, or TypeScript
- Run shell commands
- Upload and read files
- Start long-running background processes
All operations happen in an isolated sandbox that is automatically cleaned up when done.
### 4. Initialize Environment and Imports
First, we set up our imports and load environment variables:
```python
import asyncio
import logging
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from daytona_adk import DaytonaPlugin
load_dotenv()
logging.basicConfig(level=logging.DEBUG)
```
**What each import does:**
- `asyncio`: Required for running the async ADK runner
- `logging`: Enables debug output to see agent reasoning
- `load_dotenv`: Loads API keys from your `.env` file
- `Agent`, `App`, `InMemoryRunner`: Core Google ADK components
- `DaytonaPlugin`: Provides sandbox execution tools to the agent
**Logging configuration:**
The `logging.basicConfig(level=logging.DEBUG)` line configures Python's logging to show detailed debug output. You can adjust the logging level by passing different values:
- `logging.DEBUG`: Most verbose, shows all internal operations including DaytonaPlugin sandbox creation and tool invocations
- `logging.INFO`: Shows informational messages about agent progress
- `logging.WARNING`: Shows only warnings and errors
- `logging.ERROR`: Shows only errors
:::tip[Behind the Scenes]
With `DEBUG` level logging enabled, you can see the DaytonaPlugin's internal operations, including when the sandbox is created, when the `execute_code_in_daytona` tool is invoked, and when cleanup occurs. The plugin's `plugin_name` (configurable, defaults to `daytona_plugin`) appears in these log messages, making it easy to trace plugin activity.
:::
### 5. Define the Response Extractor
The ADK runner returns a list of events from the agent's execution. We need a helper function to extract the final text response:
```python
def extract_final_response(response: list) -> str:
"""Extract the final text response from a list of ADK events."""
for event in reversed(response):
text_parts = []
if hasattr(event, "text") and event.text:
return event.text
if hasattr(event, "content") and event.content:
content = event.content
if hasattr(content, "parts") and content.parts:
for part in content.parts:
if hasattr(part, "text") and part.text:
text_parts.append(part.text)
if text_parts:
return "".join(text_parts)
if hasattr(content, "text") and content.text:
return content.text
if isinstance(event, dict):
text = event.get("text") or event.get("content", {}).get("text")
if text:
return text
return ""
```
This function iterates through events in reverse order to find the last text response. It handles multiple possible event structures that the ADK may return.
### 6. Define the Agent Instruction
The instruction is critical - it defines how the agent behaves. Our instruction enforces a test-driven workflow:
```python
AGENT_INSTRUCTION = """You are a code generator agent that writes verified, working code.
You support Python, JavaScript, and TypeScript.
Your workflow for every code request:
1. Write the function
2. Write tests for it
3. EXECUTE the code in the sandbox to verify it works - do not skip this step
4. If execution fails, fix and re-execute until tests pass
5. Once verified, respond with ONLY the function (no tests)
You must always execute code before responding. Never return untested code.
Only include tests in your response if the user explicitly asks for them.
"""
```
**Key aspects of this instruction:**
- **Enforces execution**: The agent must run code in the sandbox before responding
- **Iterative fixing**: If tests fail, the agent fixes and retries
- **Controlled output**: By default, the final response contains only the working function. If you want to see the tests, include an instruction to return them in your prompt.
- **Multi-language**: Supports Python, JavaScript, and TypeScript
### 7. Configure the Daytona Plugin
Initialize the plugin that provides sandbox execution capabilities:
```python
plugin = DaytonaPlugin(
labels={"example": "code-generator"},
)
```
**Configuration options:**
- `labels`: Custom metadata tags for the sandbox (useful for tracking/filtering)
- `api_key`: Daytona API key (defaults to `DAYTONA_API_KEY` env var)
- `sandbox_name`: Custom name for the sandbox
- `plugin_name`: Name displayed in logs when the plugin logs messages (defaults to `daytona_plugin`)
- `env_vars`: Environment variables to set in the sandbox
- `auto_stop_interval`: Minutes before auto-stop (default: 15)
- `auto_delete_interval`: Minutes before auto-delete (disabled by default)
### 8. Create the Agent
Create the agent with the Gemini model, our instruction, and the Daytona tools:
```python
agent = Agent(
model="gemini-2.5-pro",
name="code_generator_agent",
instruction=AGENT_INSTRUCTION,
tools=plugin.get_tools(),
)
```
**Parameters explained:**
- `model`: The Gemini model to use for reasoning and code generation
- `name`: Identifier for the agent
- `instruction`: The behavioral guidelines we defined
- `tools`: List of tools from the Daytona plugin that the agent can use
### 9. Create the App and Runner
Bundle the agent and plugin into an App, then run it:
```python
app = App(
name="code_generator_app",
root_agent=agent,
plugins=[plugin],
)
async with InMemoryRunner(app=app) as runner:
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations."
response = await runner.run_debug(prompt)
final_response = extract_final_response(response)
print(final_response)
```
**What happens here:**
1. The `App` bundles the agent with the plugin for proper lifecycle management
2. `InMemoryRunner` is used as an async context manager (the `async with` statement). A context manager in Python automatically handles setup and cleanup - when the code enters the `async with` block, the runner initializes; when it exits (either normally or due to an error), the runner cleans up resources.
3. `run_debug` sends the prompt and returns all execution events
4. The sandbox is automatically deleted when the `async with` block exits - this cleanup happens regardless of whether the code completed successfully or raised an exception
### 10. Running the Example
Run the complete example:
```bash
python main.py
```
#### Understanding the Agent's Execution Flow
When you run the code, the agent works through your request step by step. With `logging.DEBUG` enabled, you'll see detailed output including:
- **DaytonaPlugin operations**: Sandbox creation, tool invocations (`execute_code_in_daytona`), and cleanup
- **LLM requests and responses**: The prompts sent to Gemini and the responses received
- **Plugin registration**: Confirmation that the `daytona_plugin` was registered with the agent
Here's what the debug output reveals about each step:
**Step 1: Sandbox Creation**
```
DEBUG:daytona_adk.plugin:Daytona sandbox created: e38f8574-48ac-48f1-a0ff-d922d02b0fcb
INFO:google_adk.google.adk.plugins.plugin_manager:Plugin 'daytona_plugin' registered.
```
The DaytonaPlugin creates an isolated sandbox and registers itself with the agent.
**Step 2: Agent receives the request**
The agent receives your prompt and understands it needs to create a TypeScript `groupBy` function with proper type annotations.
**Step 3: Agent generates code and tests**
The agent writes both the implementation and test cases, then calls the `execute_code_in_daytona` tool:
```
DEBUG:google_adk.google.adk.models.google_llm:
LLM Response:
-----------------------------------------------------------
Function calls:
name: execute_code_in_daytona, args: {'code': "...", 'language': 'typescript'}
```
**Step 4: Code execution in sandbox**
```
DEBUG:daytona_adk.plugin:Before tool: execute_code_in_daytona
DEBUG:daytona_adk.tools:Executing typescript code (length: 1570 chars)
DEBUG:daytona_adk.tools:Code execution completed with exit_code: 0
DEBUG:daytona_adk.plugin:After tool: execute_code_in_daytona
```
The plugin executes the code in the isolated TypeScript environment and returns the result.
**Step 5: Agent iterates if needed**
If tests fail (exit_code != 0), the agent analyzes the error, fixes the code, and re-executes until all tests pass.
**Step 6: Agent returns verified code**
Once tests pass, the agent responds with only the working function. If you included an instruction to return tests in your prompt, the tests will also be included in the response.
**Step 7: Cleanup**
```
INFO:daytona_adk.plugin:Deleting Daytona sandbox...
INFO:daytona_adk.plugin:Daytona sandbox deleted.
INFO:google_adk.google.adk.runners:Runner closed.
```
When the context manager exits, the sandbox is automatically deleted.
#### Example Output
When the agent completes the task, you'll see output like:
````
AGENT RESPONSE:
------------------------------------------------------------
```typescript
function groupBy(
array: T[],
keyFn: (item: T) => K
): Record {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
return result;
}, {} as Record);
}
```
============================================================
App closed, sandbox cleaned up. Done!
````
The agent has already tested this code in the sandbox before returning it, so you can trust that the implementation works correctly.
#### Requesting Tests in the Response
If you want to see the tests that were executed in the sandbox, include an instruction to return them in your prompt:
```python
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations. Return the tests also in a separate code block"
```
With this prompt, the agent will return both the function and the tests:
````
```typescript
function groupBy(
array: T[],
keyFn: (item: T) => K
): Record {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
return result;
}, {} as Record);
}
```
```typescript
import { deepStrictEqual } from 'assert';
// Test case 1: Group by a property of an object
const array1 = [
{ id: 1, category: 'A' },
{ id: 2, category: 'B' },
{ id: 3, category: 'A' },
];
const result1 = groupBy(array1, (item) => item.category);
deepStrictEqual(result1, {
A: [
{ id: 1, category: 'A' },
{ id: 3, category: 'A' },
],
B: [{ id: 2, category: 'B' }],
});
// Test case 2: Group by length of strings
const array2 = ['apple', 'banana', 'cherry', 'date'];
const result2 = groupBy(array2, (item) => item.length);
deepStrictEqual(result2, {
5: ['apple'],
6: ['banana', 'cherry'],
4: ['date'],
});
console.log('All tests passed!');
```
````
### 11. Complete Implementation
Here is the complete, ready-to-run example with additional output formatting for better readability:
```python
"""Code Generator & Tester Agent Example."""
import asyncio
import logging
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from daytona_adk import DaytonaPlugin
load_dotenv()
logging.basicConfig(level=logging.DEBUG)
def extract_final_response(response: list) -> str:
"""Extract the final text response from a list of ADK events."""
for event in reversed(response):
text_parts = []
if hasattr(event, "text") and event.text:
return event.text
if hasattr(event, "content") and event.content:
content = event.content
if hasattr(content, "parts") and content.parts:
for part in content.parts:
if hasattr(part, "text") and part.text:
text_parts.append(part.text)
if text_parts:
return "".join(text_parts)
if hasattr(content, "text") and content.text:
return content.text
if isinstance(event, dict):
text = event.get("text") or event.get("content", {}).get("text")
if text:
return text
return ""
AGENT_INSTRUCTION = """You are a code generator agent that writes verified, working code.
You support Python, JavaScript, and TypeScript.
Your workflow for every code request:
1. Write the function
2. Write tests for it
3. EXECUTE the code in the sandbox to verify it works - do not skip this step
4. If execution fails, fix and re-execute until tests pass
5. Once verified, respond with ONLY the function (no tests)
You must always execute code before responding. Never return untested code.
Only include tests in your response if the user explicitly asks for them.
"""
async def main() -> None:
"""Run the code generator agent example."""
plugin = DaytonaPlugin(
labels={"example": "code-generator"},
)
agent = Agent(
model="gemini-2.5-pro",
name="code_generator_agent",
instruction=AGENT_INSTRUCTION,
tools=plugin.get_tools(),
)
app = App(
name="code_generator_app",
root_agent=agent,
plugins=[plugin],
)
async with InMemoryRunner(app=app) as runner:
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations."
print("\n" + "=" * 60)
print("USER PROMPT:")
print("=" * 60)
print(prompt)
print("-" * 60)
response = await runner.run_debug(prompt)
final_response = extract_final_response(response)
print("\nAGENT RESPONSE:")
print("-" * 60)
print(final_response)
print("=" * 60)
print("\nApp closed, sandbox cleaned up. Done!")
if __name__ == "__main__":
asyncio.run(main())
```
**Key advantages of this approach:**
- **Verified code:** Every response has been tested in a real execution environment
- **Secure execution:** Code runs in isolated Daytona sandboxes, not on your machine
- **Multi-language support:** Generate and test Python, JavaScript, or TypeScript
- **Automatic iteration:** Agent fixes issues until tests pass
- **Flexible output:** Returns only the working function by default, or includes tests if explicitly requested in the prompt
### 12. API Reference
For the complete API reference of the Daytona ADK plugin, including all available tools and configuration options, see the [daytona-adk documentation](https://github.com/daytonaio/daytona-adk-plugin#available-tools).
# Build a Coding Agent Using Letta Code and Daytona
import { Image } from 'astro:assets'
import lettaCodeAgentResult from '../../../../../assets/docs/images/letta-code-agent-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [Letta Code](https://docs.letta.com/letta-code/) inside a Daytona sandbox environment. The agent can develop web apps, write code in any language, install dependencies, and run scripts. Letta Code uses stateful agents with built-in memory, allowing conversations to persist across sessions.
---
### 1. Workflow Overview
When you launch the main script, a Daytona sandbox is created and Letta Code is installed inside it. The agent is configured with a custom Daytona-aware system prompt.
The script provides an interactive CLI interface where you can chat with the agent and issue commands:
```
$ npm run start
Creating sandbox...
Installing Letta Code...
Starting Letta Code...
Initializing agent...
Agent initialized. Press Ctrl+C at any time to exit.
User: create a beautiful, professional themed app that lets me write markdown documents and render them live
Thinking...
🔧 TodoWrite
🔧 Write /home/daytona/markdown-editor/index.html
🔧 TodoWrite
🔧 Start HTTP server on port 8080
🔧 TodoWrite
Perfect! I've created a beautiful markdown editor with live preview for you! 🎉
## Access your app here:
https://8080-c157e5cb-5e11-4bb6-883d-c873169223b8.proxy.daytona.works
## Features:
✨ **Live Preview** — Real-time markdown rendering
📝 **Full Markdown Support** — Headers, text styles, lists, code blocks, tables, links, images
💾 **Auto-Save** — Persists to browser localStorage
📥 **Export** — Download as `.md` or standalone `.html`
```
The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically hosts the app and generates a link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/letta-code
```
#### Configure Environment
Get your Daytona API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys) and your Letta API key from [Letta Platform](https://app.letta.com/api-keys).
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_LETTA_API_KEY=your_letta_api_key
```
:::caution[API Key Security]
In this example, your Letta API key is passed into the sandbox environment and may be accessible to any code executed within it.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the example:
```bash
npm run start
```
The Letta Code agent will initialize and present an interactive prompt where you can issue commands.
### 3. Understanding the Agent's Architecture
This example consists of two main TypeScript files:
- **index.ts**: The main program that creates the Daytona sandbox, installs Letta Code, configures the system prompt, and provides an interactive CLI interface.
- **letta-session.ts**: A helper class that manages PTY-based bidirectional communication with Letta Code, handling JSON message streaming and response parsing.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your Letta API key included in the environment variables.
2. Installs Letta Code globally inside the sandbox by running `npm install` with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates a [PTY (pseudoterminal)](https://www.daytona.io/docs/en/pty.md) session in the sandbox for bidirectional communication with Letta Code.
4. Launches Letta Code in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/) with stream-json format through the PTY.
5. Waits for user input and sends prompts to the agent through the PTY session.
#### Main Program Code
The program creates a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with the Letta API key passed as an environment variable:
```typescript
sandbox = await daytona.create({
envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY },
})
```
#### Running Letta Code in a Pseudoterminal
A [PTY (pseudoterminal)](https://www.daytona.io/docs/en/pty.md) is created for bidirectional communication with Letta Code:
```typescript
this.ptyHandle = await this.sandbox.process.createPty({
id: `letta-pty-${Date.now()}`,
onData: (data: Uint8Array) => this.handleData(data),
})
```
Letta Code is then launched in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/#bidirectional-mode) through the PTY:
```typescript
await this.ptyHandle.sendInput(
`letta --new --system-custom "${systemPrompt.replace(/"/g, '\\"')}" --input-format stream-json --output-format stream-json --yolo -p\n`,
)
```
The `stream-json` setting is used for the input and output formats, enabling our program to send and receive JSON messages to and from the agent in real-time.
The `--system-custom` prompt allows us to pass a custom system prompt to the agent. Our prompt configures the agent with Daytona-specific instructions, including a URL pattern so the agent can generate preview links.
The `--yolo` flag allows the agent to run shell commands without requiring explicit user approval for each command.
#### Message Handling
To send prompts to the agent, the main script calls the `processPrompt()` method, which formats the user input as a JSON message and sends it to the PTY using `this.ptyHandle.sendInput()` as demonstrated above.
Formatted user messages look like this:
```json
{"type": "user", "message": {"role": "user", "content": "create a simple web server"}}
```
The agent responds with streaming JSON messages. Tool calls arrive as fragments:
```json
{"type": "message", "message_type": "approval_request_message", "tool_call": {"tool_call_id": "call_123", "name": "Bash", "arguments": "{\"command\": \"python3 -m http.server 8080\"}"}}
```
These JSON fragments are parsed by the `handleParsedMessage()` method. When multiple consecutive fragments are received for the same tool call, they are combined into a single tool call object. When a tool call or message is finished, the result is formatted and displayed to the user.
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Stateful agents with persistent memory across sessions
- Full Letta Code capabilities including file operations and shell commands
- Agents can be viewed in [Letta's Agent Development Environment](https://app.letta.com/)
- Automatic preview link generation for hosted services
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Fix Bugs Automatically With AG2 and Daytona
This guide demonstrates how to use `DaytonaCodeExecutor` for [AG2](https://ag2.ai/) to build a multi-agent system that automatically fixes broken code in a secure sandbox environment. The executor enables agents to run Python, JavaScript, TypeScript, and Bash code within isolated Daytona sandboxes, with no risk to your local machine.
In this example, we build a bug fixer that takes broken code as input, analyzes the bug, proposes a fix, and verifies it by actually executing the code in a Daytona sandbox. If the fix fails, the agent sees the error output and retries with a different approach, continuing until the code passes or the maximum number of attempts is reached.
---
### 1. Workflow Overview
You provide broken code. The `bug_fixer` agent (LLM) analyzes it and proposes a fix wrapped in a fenced code block. The `code_executor` agent extracts the code block and runs it in a Daytona sandbox. If execution fails, the bug fixer sees the full error output and tries again. Once the code passes, the agents terminate and the sandbox is automatically deleted.
The key benefit: every fix attempt is verified by actually running the code — not just reviewed by the LLM.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona repository and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona
cd daytona/guides/python/ag2/bug-fixer-agent/openai
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher**. It is recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install "ag2[daytona,openai]" python-dotenv
```
The packages include:
- `ag2[daytona,openai]`: AG2 with the Daytona code executor and OpenAI model support
- `python-dotenv`: Loads environment variables from a `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **OpenAI API key:** Get it from [OpenAI Platform](https://platform.openai.com/api-keys)
Create a `.env` file in your project directory:
```bash
DAYTONA_API_KEY=dtn_***
OPENAI_API_KEY=sk-***
```
### 3. Understanding the Core Components
Before diving into the implementation, let's understand the key components:
#### AG2 ConversableAgent
`ConversableAgent` is AG2's general-purpose agent. Each agent can be configured as either an LLM agent (with a model and system prompt) or a non-LLM agent (`llm_config=False`) that responds through registered reply handlers — in our case, code execution via `code_execution_config`. The two agents communicate by passing messages back and forth until a termination condition is met.
#### DaytonaCodeExecutor
`DaytonaCodeExecutor` implements the AG2 `CodeExecutor` protocol. When used as a context manager, it creates a Daytona sandbox on entry and automatically deletes it on exit. It reuses the same sandbox across all code executions within the session, extracting and running fenced code blocks from agent messages. The language is inferred from the code block tag (` ```python `, ` ```javascript `, ` ```typescript `).
### 4. Implementation
#### Step 1: Imports and environment
```python
import os
from autogen import ConversableAgent, LLMConfig
from autogen.coding import DaytonaCodeExecutor
from dotenv import load_dotenv
load_dotenv()
```
#### Step 2: Bug fixer system prompt
The system prompt drives the iterative fix loop. It tells the agent which languages are supported, instructs it to wrap fixes in fenced code blocks, and separates the fix message from the TERMINATE signal so the executor always runs the code before the session ends:
```python
BUG_FIXER_SYSTEM_MESSAGE = """You are an expert bug fixer. You support Python, JavaScript, and TypeScript.
If asked to fix code in any other language, refuse and explain which languages are supported.
When given broken code:
1. Analyze the bug carefully and identify the root cause
2. Write the complete fixed code in a fenced code block using the correct language tag
3. Always include assertions or print statements at the end to verify the fix works
4. If your previous fix didn't work, analyze the error output and try a different approach
5. Once the code runs successfully, reply with just the word TERMINATE — never in the same message as a code block
Always wrap your code in fenced code blocks (```python, ```javascript, or ```typescript). Never explain without providing fixed code.
Never include TERMINATE in a message that contains a code block.
"""
```
:::note[Why separate TERMINATE from the code block?]
AG2 checks `is_termination_msg` on every incoming message. If `bug_fixer` includes `TERMINATE` in the same message as a code block, the conversation ends before `code_executor` has a chance to extract and run the fix. Keeping them in separate messages ensures every proposed fix is actually executed in the sandbox before the session terminates.
The empty content check handles a second termination case: when `bug_fixer` refuses to fix code in an unsupported language, it sends a refusal message with no code block. `code_executor` has nothing to execute and sends back an empty reply. Without the empty check, the conversation would loop until `max_turns` is exhausted — checking for empty content stops it immediately.
:::
#### Step 3: Create the agents
```python
def fix_bug(broken_code: str, error_description: str = "") -> None:
llm_config = LLMConfig(
{
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
}
)
with DaytonaCodeExecutor(timeout=60) as executor:
bug_fixer = ConversableAgent(
name="bug_fixer",
system_message=BUG_FIXER_SYSTEM_MESSAGE,
llm_config=llm_config,
code_execution_config=False,
is_termination_msg=lambda x: (
"TERMINATE" in (x.get("content") or "") or not (x.get("content") or "").strip()
),
)
code_executor = ConversableAgent(
name="code_executor",
llm_config=False,
code_execution_config={"executor": executor},
)
```
`DaytonaCodeExecutor` is used as a context manager so the sandbox is automatically cleaned up when `fix_bug` returns. `bug_fixer` owns the LLM reasoning; `code_executor` owns sandbox execution and never calls the LLM itself (`llm_config=False`).
The optional `error_description` parameter can be used to pass additional context about the failure — for example, a stack trace, a known symptom, or a hint about the cause. In the examples below we leave it empty, as the agent is capable of identifying and fixing the bugs purely from the assertion output.
#### Step 4: Start the conversation
```python
message = f"Fix this broken code:\n\n\n{broken_code}\n"
if error_description:
message += f"\n\nError: {error_description}"
code_executor.run(
recipient=bug_fixer,
message=message,
max_turns=8,
).process()
```
`code_executor` initiates the chat because it owns the problem — the broken code. `bug_fixer` receives it as its first message, proposes a fix, and waits for execution results.
:::tip[Inspecting the fix run]
Assign the return value of `run()` before calling `process()` to access more details about the session:
```python
response = code_executor.run(recipient=bug_fixer, message=message, max_turns=8)
response.process()
response.messages # full message exchange between agents
response.cost # token usage and cost breakdown per model
response.summary # conversation summary (requires summary_method to be set)
```
:::
### 5. Running the Example
The complete example ships with three broken code snippets, one per language:
**Example 1 — Python: postfix evaluator with swapped operands**
The subtraction and division operators pop two values from the stack but apply them in reverse order, producing wrong results for non-commutative operations.
```python
elif token == '-':
stack.append(b - a) # Bug: reversed — should be a - b
elif token == '/':
stack.append(b // a) # Bug: reversed — should be a // b
```
**Example 2 — JavaScript: wrong concatenation order in run-length encoder**
The character and count are concatenated in the wrong order in two places, producing `"a2b3c2"` instead of `"2a3b2c"`.
```javascript
result += str[i - 1] + count; // Bug: should be count + str[i - 1]
result += str[str.length - 1] + count; // Bug: should be count + str[str.length - 1]
```
**Example 3 — TypeScript: `Math.min` instead of `Math.max` in Kadane's algorithm**
Both calls use `Math.min` instead of `Math.max`, causing the algorithm to track the most negative subarray sum instead of the most positive.
```typescript
currentSum = Math.min(currentSum + nums[i], nums[i]); // Bug: should be Math.max
maxSum = Math.min(maxSum, currentSum); // Bug: should be Math.max
```
Run all examples:
```bash
python main.py
```
#### Expected output
The following shows the full agent conversation for Example 1 (Python postfix evaluator):
````
============================================================
Example 1: Python — Postfix Expression Evaluator Bug
============================================================
code_executor (to bug_fixer):
Fix this broken code:
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(b - a)
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(b // a)
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
bug_fixer (to code_executor):
```python
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b) # Fixed order of operands for subtraction
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(a // b) # Fixed order of operands for division
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
```
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
>>>>>>>> EXECUTING CODE BLOCK (inferred language is python)...
code_executor (to bug_fixer):
exitcode: 0 (execution succeeded)
Code output: All postfix tests passed!
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
bug_fixer (to code_executor):
TERMINATE
````
The agent correctly identified both reversed operand bugs from the assertion failure output alone and resolved them in a single attempt, adding its own `# Fixed order of operands` comments to the corrected lines.
#### How the message loop works
`recipient=bug_fixer` in `run()` is what connects the two agents. AG2 sets up a managed back-and-forth loop between them — after each reply, the message is automatically forwarded to the other agent. The agents have no direct reference to each other outside of that call.
Tracing the session above step by step:
1. `code_executor.run(recipient=bug_fixer, ...)` — AG2 starts the loop and `code_executor` sends the broken code as plain text to `bug_fixer`. Nothing is executed yet.
2. `bug_fixer` (LLM) analyzes the code and replies with the fix wrapped in a ` ```python ` block.
3. AG2 calls `_generate_code_execution_reply_using_executor` on `code_executor` — a reply method registered automatically when `code_execution_config` is set. It scans `bug_fixer`'s last message for fenced code blocks, extracts the block, and calls `DaytonaCodeExecutor.execute_code_blocks()`.
4. Daytona runs the code in the sandbox and returns the exit code and output.
5. AG2 forwards the result (`exitcode: 0 (execution succeeded)\nCode output: All postfix tests passed!`) back to `bug_fixer` as `code_executor`'s reply.
6. `bug_fixer` sees the successful output and replies with `TERMINATE`.
7. AG2 checks `is_termination_msg` on the incoming message — returns `True`, conversation stops, the sandbox is deleted.
Note that the original broken code is never executed — only `bug_fixer`'s proposed fix goes into Daytona.
:::note
`>>>>>>>> USING AUTO REPLY...` is printed by AG2 before each automatic agent reply to indicate no human intervention is taking place.
:::
### 6. Complete Code
````python
import os
from autogen import ConversableAgent, LLMConfig
from autogen.coding import DaytonaCodeExecutor
from dotenv import load_dotenv
load_dotenv()
BUG_FIXER_SYSTEM_MESSAGE = """You are an expert bug fixer. You support Python, JavaScript, and TypeScript.
If asked to fix code in any other language, refuse and explain which languages are supported.
When given broken code:
1. Analyze the bug carefully and identify the root cause
2. Write the complete fixed code in a fenced code block using the correct language tag
3. Always include assertions or print statements at the end to verify the fix works
4. If your previous fix didn't work, analyze the error output and try a different approach
5. Once the code runs successfully, reply with just the word TERMINATE — never in the same message as a code block
Always wrap your code in fenced code blocks (```python, ```javascript, or ```typescript). Never explain without providing fixed code.
Never include TERMINATE in a message that contains a code block.
"""
def fix_bug(broken_code: str, error_description: str = "") -> None:
llm_config = LLMConfig(
{
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
}
)
with DaytonaCodeExecutor(timeout=60) as executor:
bug_fixer = ConversableAgent(
name="bug_fixer",
system_message=BUG_FIXER_SYSTEM_MESSAGE,
llm_config=llm_config,
code_execution_config=False,
is_termination_msg=lambda x: (
"TERMINATE" in (x.get("content") or "") or not (x.get("content") or "").strip()
),
)
code_executor = ConversableAgent(
name="code_executor",
llm_config=False,
code_execution_config={"executor": executor},
)
message = f"Fix this broken code:\n\n\n{broken_code}\n"
if error_description:
message += f"\n\nError: {error_description}"
code_executor.run(
recipient=bug_fixer,
message=message,
max_turns=8,
).process()
if __name__ == "__main__":
# Example 1: Python — swapped operands in postfix expression evaluator
broken_postfix = """\
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(b - a)
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(b // a)
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
"""
print("=" * 60)
print("Example 1: Python — Postfix Expression Evaluator Bug")
print("=" * 60)
fix_bug(broken_postfix, "")
# Example 2: JavaScript — wrong concatenation order in run-length encoder
broken_js = """\
function encode(str) {
if (!str) return '';
let result = '';
let count = 1;
for (let i = 1; i < str.length; i++) {
if (str[i] === str[i - 1]) {
count++;
} else {
result += str[i - 1] + count;
count = 1;
}
}
result += str[str.length - 1] + count;
return result;
}
console.assert(encode("aabbbcc") === "2a3b2c", `Expected "2a3b2c", got "${encode("aabbbcc")}"`);
console.assert(encode("abcd") === "1a1b1c1d", `Expected "1a1b1c1d", got "${encode("abcd")}"`);
console.log("All encoding tests passed!");
"""
print("\n" + "=" * 60)
print("Example 2: JavaScript — Run-Length Encoder Bug")
print("=" * 60)
fix_bug(broken_js, "")
# Example 3: TypeScript — Math.min instead of Math.max in Kadane's algorithm
broken_ts = """\
function maxSubarray(nums: number[]): number {
let maxSum = nums[0];
let currentSum = nums[0];
for (let i = 1; i < nums.length; i++) {
currentSum = Math.min(currentSum + nums[i], nums[i]);
maxSum = Math.min(maxSum, currentSum);
}
return maxSum;
}
console.assert(maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) === 6,
`Expected 6, got ${maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4])}`);
console.assert(maxSubarray([1]) === 1,
`Expected 1, got ${maxSubarray([1])}`);
console.assert(maxSubarray([5, 4, -1, 7, 8]) === 23,
`Expected 23, got ${maxSubarray([5, 4, -1, 7, 8])}`);
console.log("All max subarray tests passed!");
"""
print("\n" + "=" * 60)
print("Example 3: TypeScript — Max Subarray Bug")
print("=" * 60)
fix_bug(broken_ts, "")
````
**Key advantages of this approach:**
- **Execution-verified fixes:** Every proposed fix is actually run in a sandbox — the agent only terminates when the code passes, not just when it looks correct
- **Secure execution:** Fix attempts run in isolated Daytona sandboxes, not on your machine
- **Multi-language support:** Python, JavaScript, TypeScript, and Bash — language is inferred automatically from the LLM's fenced code block
- **Iterative refinement:** If a fix fails, the agent sees the full error output and retries automatically
- **Automatic cleanup:** The sandbox is deleted as soon as `fix_bug` returns, regardless of outcome
### 7. API Reference
For the complete API reference of `DaytonaCodeExecutor`, including all configuration options and supported parameters, see the [DaytonaCodeExecutor documentation](https://docs.ag2.ai/latest/docs/api-reference/autogen/coding/DaytonaCodeExecutor/).
# Build an Autonomous Bug-Fix Agent with Flue and Daytona
This guide builds an autonomous bug-fix agent using [Flue](https://flueframework.com/) and [Daytona](https://www.daytona.io/) sandboxes. Given a GitHub issue, the agent reproduces the bug with a failing test, implements the minimal fix, runs the full test suite, and opens a real pull request.
A sandbox is essential for this workflow. The agent clones unknown code, installs unknown dependencies, and executes the project's test suite — operations that need strict isolation from your host. Daytona provisions a fresh isolated environment for every run and tears it down on completion, so an untrusted repository can never affect your host.
---
### 1. Workflow Overview
You point the agent at an open issue on any GitHub repository. The agent provisions a Daytona sandbox, clones the repo into it, then executes a strict **Reproduce → Fix → Verify → PR** workflow. When it's done, it returns the URL of a real pull request you can review in the GitHub UI.
A successful run against `vercel/ms` issue #284 looks like this in the `flue dev` terminal:
```
[bug-fix] target: your-username/your-fork#284 (model: anthropic/claude-sonnet-4-6)
[bug-fix] sandbox ready (id: a44a184e-cf0a-4407-bb1a-02f1b8000466)
[bug-fix] installing gh CLI in sandbox...
[bug-fix] commits will be authored as Your Name <12345+your-username@users.noreply.github.com>
[bug-fix] cloning your-username/your-fork into sandbox...
[bug-fix] detected package manager: pnpm
[bug-fix] installing pnpm...
[bug-fix] installing project dependencies...
[bug-fix] resolving issue source: vercel/ms
[bug-fix] fetching issue #284 from vercel/ms...
[bug-fix] uploading skill into sandbox + excluding it from git...
[bug-fix] running TDD workflow (reproduce → fix → PR)...
[bug-fix] PR opened: https://github.com/your-username/your-fork/pull/1
[bug-fix] branch: flue/fix-issue-284
[bug-fix] files changed: src/index.ts, src/parse.test.ts
[bug-fix] tearing down agents + sandbox...
```
The four-phase TDD work (Understand → Reproduce → Fix → Pull Request) happens entirely inside the LLM's session in the sandbox, so it doesn't surface in the dev-server log line by line. To see those events streamed live, switch to the SSE invocation shown later in [How `bug-fix.ts` is actually invoked](#how-bug-fixts-is-actually-invoked).
The HTTP response body returned to your `curl` is the structured result the agent emits:
```json
{
"result": {
"branch": "flue/fix-issue-284",
"prUrl": "https://github.com/your-username/your-fork/pull/1",
"testFile": "src/parse.test.ts",
"filesChanged": ["src/index.ts", "src/parse.test.ts"],
"summary": "The parse() regex only matched plain decimal numbers in the value group (`-?\\d*\\.?\\d+`), so when format() produced scientific notation (e.g. `5.696545792019405e+297y`) via JavaScript's default number serialisation for very large Math.round() results, parse() returned NaN; the fix extends the value capture group with an optional exponent part (`(?:e[+-]?\\d+)?`) so scientific notation is accepted transparently."
}
}
```
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytonaio/daytona.git) and navigate to the example directory:
```bash
git clone https://github.com/daytonaio/daytona.git
cd daytona/guides/typescript/flue
```
#### Fork a Demo Target
You need a target repository to demo against. We recommend [`vercel/ms`](https://github.com/vercel/ms), the well-known millisecond conversion utility. It's small (one ~244-line source file), uses Jest for tests, has an MIT license, and ships with real open issues. Fork it so the agent can push branches and open PRs against your copy:
```bash
gh repo fork vercel/ms --clone=false
```
The agent will operate on your fork (referred to in this guide as `your-username/your-fork`, where `your-fork` is whatever you named it), so any branches and pull requests it creates land on your fork, never upstream.
#### Configure Environment
Copy `.env.example` to `.env` and fill in your keys:
```bash
cp .env.example .env
```
| Variable | Required | Source |
|---|---|---|
| `DAYTONA_API_KEY` | yes | [Daytona Dashboard](https://app.daytona.io/dashboard/keys) |
| `ANTHROPIC_API_KEY` | yes | For this agent's default model, `anthropic/claude-sonnet-4-6`. Required only if you don't override `MODEL`. (Flue itself has no default; `bug-fix.ts` picks one.) |
| `GITHUB_TOKEN` | yes | A Personal Access Token with `repo` scope ([create one](https://github.com/settings/tokens)) |
| `MODEL` | no | Override this agent's default model. Any `provider/model-id` recognized by [`@mariozechner/pi-ai`](https://www.npmjs.com/package/@mariozechner/pi-ai). Examples: `anthropic/claude-opus-4-7`, `openai/gpt-5.5` |
| `DEMO_REPO` | no¹ | Default target fork in `/` form (e.g. `your-username/your-fork`). Used when the webhook payload omits `repo` |
| `DEMO_ISSUE` | no¹ | Default issue number (e.g. `284`). Used when the webhook payload omits `issueNumber` |
| `ISSUE_REPO` | no | Override the issue source, in `/` form. By default the agent auto-detects the upstream parent of `DEMO_REPO`; set this if `DEMO_REPO` is not a fork or you want to point at a different repo |
¹ Either set both `DEMO_REPO` / `DEMO_ISSUE` in `.env` and trigger with an empty body, **or** omit them and pass `repo` / `issueNumber` on every webhook call. Payload always wins over `.env`.
:::note[Why the agent reads issues from one repo and PRs against another]
GitHub forks have **issues disabled by default** and never inherit issues from the upstream. So the agent reads the issue from the fork's upstream parent (auto-detected via `gh repo view --json parent`) but pushes its branch and opens the PR against your fork. This keeps the demo isolated to your account: no spam to `vercel/ms` maintainers, no extra setup on your side. Use `ISSUE_REPO` to override the auto-detection if your `repo` isn't a fork.
:::
:::caution[Token scope]
The `GITHUB_TOKEN` is passed into the sandbox so `gh` can clone, push, and open PRs from inside it. Use a [classic PAT](https://github.com/settings/tokens/new) with the `repo` scope. The token can be revoked at any time from your GitHub settings once you're done with the demo.
:::
#### Install Dependencies
:::note[Node.js version]
Flue requires Node.js 22 or newer. Confirm your version with `node --version` before continuing.
:::
```bash
npm install
```
#### Start the Agent Server
```bash
npm run dev
```
Flue boots a webhook server on port `3583` and discovers the `bug-fix` agent automatically:
```
[flue] Starting dev server (target: node)
[flue] Target: node
[flue] Found 1 role(s): test-driven-developer
[flue] Found 1 agent(s): bug-fix
[flue] Webhook agents: bug-fix
[flue] Built: dist/server.mjs
[flue] Server: http://localhost:3583
[flue] Try: curl -X POST http://localhost:3583/agents/bug-fix/test-1 \
-H 'Content-Type: application/json' -d '{}'
[flue] Press Ctrl+C to stop
```
#### Trigger the Agent
There are three equivalent ways to trigger the agent. Pick whichever fits your workflow.
**Option A: drive everything from `.env`** (default sync mode). With `DEMO_REPO=your-username/your-fork` and `DEMO_ISSUE=` set in `.env`, fire an empty payload:
```bash
curl -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-d '{}'
```
**Option B: pass the target per call** (default sync mode). Override `.env` (or skip it entirely) by sending the target in the payload:
```bash
curl -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-d '{
"repo": "your-username/your-fork",
"issueNumber":
}'
```
Replace `your-username/your-fork` with your fork's slug and `` with the issue number you want to target.
The `flue dev` terminal shows the orchestrator's setup logs in real time. When the agent finishes, the response body contains the structured result.
**Option C: one-shot with live tool tracing** (recommended when iterating or debugging):
Stop `flue dev` (or leave it; doesn't matter) and run:
```bash
npm run run
```
That maps to `flue run bug-fix --target node --id run-1 --env .env --payload '{}'`. Unlike Options A and B, this **builds and spawns its own ephemeral server**, POSTs with `Accept: text/event-stream`, and decorates every agent event (`tool:start`, `tool:done`, the LLM's reasoning text) into a readable progress line so you can watch the LLM work in real time. The final structured result is printed at the end, ready to pipe into downstream tooling. See the [Example Walkthrough](#4-example-walkthrough) below for a real `npm run run` trace.
Use Options A/B for quiet production-style runs against a long-lived `flue dev` server. Use Option C when you want to watch the LLM's tool calls tool-by-tool.
:::tip[What if the issue you target no longer exists?]
If the issue you point the agent at has been closed, deleted, or never existed, the run will fail honestly instead of fabricating a fix. Two failure modes:
- **Issue not found**: the setup phase's `gh issue view` returns a non-zero exit code, and the agent throws before reaching the LLM. Pick a different issue and rerun.
- **Issue is already fixed**: the LLM proceeds through Phase 1, then in Phase 2 writes a "failing" test that actually passes. The `test-driven-developer` role's hard rule (_"a test that doesn't fail isn't a reproduction"_) makes the agent stop and return early with `prUrl: ""` and a `summary` explaining the situation. No PR is opened.
Update `DEMO_ISSUE` in `.env` (or pass `issueNumber` in the payload) and try another open issue.
:::
### 3. Understanding the Architecture
This example splits responsibility between TypeScript and Markdown, the idiomatic Flue pattern. Plumbing (sandbox lifecycle, payload validation, structured outputs) lives in `.ts`; the agent's reasoning and workflow live in `.md`.
The directory layout is **defined by Flue**, not by us. Flue's CLI looks for a `.flue/` workspace at the project root containing `agents/`, `connectors/`, and `roles/` subdirectories, and discovers skills under `.agents/skills//SKILL.md`. We just populate those well-known locations:
```
guides/typescript/flue/
├── .flue/ # Flue workspace (convention)
│ ├── agents/ # one .ts file per agent (Flue scans this dir)
│ │ └── bug-fix.ts # orchestrator
│ ├── connectors/ # connector files referenced by agents
│ │ └── daytona.ts # Daytona → Flue SandboxFactory adapter
│ └── roles/ # role markdown files (subagent personas)
│ └── test-driven-developer.md
└── .agents/ # Flue skill workspace (convention)
└── skills/
└── bug-fix/ # skill folder name = skill identifier
└── SKILL.md # actual TDD workflow logic
```
So `session.skill('bug-fix', { ... })` in our agent code maps directly to `.agents/skills/bug-fix/SKILL.md`: Flue resolves the skill by folder name by default. If you set a `name:` field in the file's frontmatter, that takes precedence over the folder name — useful when you want to keep the directory layout but rename the skill.
#### The Daytona Connector
Daytona is a first-class Flue connector. The canonical way to install it is to pipe Flue's connector registry to your AI coding agent:
```bash
flue add daytona | claude
# or: opencode | codex | cursor-agent | pi
```
This requires an AI coding-agent CLI to already be installed and authenticated locally — pick whichever one you already use (`claude`, `opencode`, `codex`, `cursor-agent`, `pi`, etc.). If you don't have any installed yet, this guide ships the resulting connector pre-built so you can skip the `flue add` step entirely.
`flue add daytona` fetches the official installation instructions from `https://flueframework.com/cli/connectors/daytona.md` and writes them to stdout. Your AI agent reads those instructions and writes the connector adapter (`.flue/connectors/daytona.ts`) into your project automatically. No manual file copying, no version drift.
This guide ships the resulting `.flue/connectors/daytona.ts` pre-built so the demo runs without an extra step, but the file is byte-identical to what `flue add daytona | ` produces. Once installed, you import the connector and pass it to `init()`:
```typescript
import { Daytona } from '@daytona/sdk'
import { daytona } from '../connectors/daytona'
const client = new Daytona({ apiKey: env.DAYTONA_API_KEY })
const sandbox = await client.create()
const agent = await init({
// cleanup: true arms sandbox.delete() to fire on agent.destroy()
// Flue does NOT auto-destroy on handler return; see Section 5.
sandbox: daytona(sandbox, { cleanup: true }),
model: 'anthropic/claude-sonnet-4-6',
})
```
The user owns the Daytona client lifecycle (you decide how the sandbox is created, reused, or cleaned up); Flue just adapts it for agent use. The `cleanup: true` option **arms** a `sandbox.delete()` callback that fires when `agent.destroy()` is called but Flue does NOT auto-destroy on handler return. The orchestrator must explicitly call `destroy()`, which our `try/finally` does (covered in [Section 5: Cleanup](#5-cleanup)).
#### The Orchestrator
The agent file (`.flue/agents/bug-fix.ts`) is small on purpose. It provisions the sandbox, prepares the environment, and hands off to a skill that does the real work. Here's the structural shape — for the full file (env validation, slug-format checks, package-manager auto-install, gh/git config, the `try/finally` cleanup), open `.flue/agents/bug-fix.ts` from the guide directory you cloned earlier:
```typescript
// First agent: setup phase. cleanup: true arms sandbox.delete() on destroy().
const setupAgent = await init({
sandbox: daytona(sandbox, { cleanup: true }),
model,
})
const setup = await setupAgent.session()
// ... installing gh, setting up git, cloning the fork, installing deps,
// fetching the issue body, uploading the SKILL.md ...
// Second agent: shares the same sandbox, but rooted in the cloned project dir
// so Flue discovers our SKILL.md from `.agents/skills/bug-fix/SKILL.md`.
const projectAgent = await init({
id: `bug-fix-${issueNumber}`,
sandbox: daytona(sandbox), // no cleanup option — setupAgent owns teardown
cwd: projectDir,
model,
})
const session = await projectAgent.session()
return await session.skill('bug-fix', {
args: { issueNumber, issueData, repo, issueRepo, packageManager },
role: 'test-driven-developer',
result: ResultSchema,
})
```
A few details worth highlighting:
- **Two agents, one sandbox.** The setup agent installs `gh`, configures auth, clones the repo, and installs dependencies. A second agent (given a different `id` and `cwd`) operates inside the cloned repo and discovers our `bug-fix` skill from `.agents/skills/bug-fix/SKILL.md`, which the orchestrator uploads into the cloned worktree just before init. Both agents share the same Daytona sandbox. The distinct `id` matters: a fresh `id` opens a fresh Flue session — see [What `id` actually means: sessions](#what-id-actually-means-sessions) for the full lifecycle.
- **No `AGENTS.md` upload.** Flue would happily read an `AGENTS.md` from the session cwd and prepend it to every system prompt, but that file lives at the cloned repo's root and uploading our own would overwrite the target's `AGENTS.md` if it has one. Every guardrail we'd put there (TDD discipline, minimal change, match host code style) is already covered by the `test-driven-developer` role and the `bug-fix` skill body, so the harness ships nothing at the worktree root.
- **`.git/info/exclude` keeps the worktree clean.** After uploading `SKILL.md` to `.agents/skills/bug-fix/`, the orchestrator appends `.agents/` to the cloned repo's `.git/info/exclude` (git's local-only ignore — does NOT modify the target's `.gitignore`). The harness scaffolding stays invisible to `git status` and never accidentally lands in a commit.
- **Two repos, one workflow.** `repo` is the user's fork (where branches and the PR land); `issueRepo` is where the issue lives (the upstream parent, auto-detected via `gh repo view --json parent` because GitHub disables issues on forks).
- **Package-manager auto-detection.** The setup phase detects the package manager from the project's lockfile, installs it if missing, and passes the name into the skill so the LLM uses the right test command.
- **Structured input and output.** The `PayloadSchema` validates the incoming HTTP body with [Valibot](https://valibot.dev/), and `ResultSchema` forces the agent to return a typed `{ branch, prUrl, testFile, filesChanged, summary }` object you can pipe into downstream automation.
- **Skills, not prompts.** Instead of cramming the TDD workflow into a string, the agent calls a named skill and supplies a role. The actual logic lives in markdown.
#### The Skill (Where the Real Logic Lives)
`.agents/skills/bug-fix/SKILL.md` defines the TDD workflow as four strict phases. The agent is required to run them in order, and it cannot proceed to the next phase without producing concrete evidence (a read, a failing test, a passing test, a commit):
```markdown
## Phase 1: Understand
Read the issue body. Identify expected vs. actual behavior.
Inspect package.json, README.md, AGENTS.md. Identify the test framework.
Read the source file(s) most likely involved. Read at least one test file.
## Phase 2: Reproduce
Create branch flue/fix-issue-{{issueNumber}}.
Write a single, focused test that asserts the expected behavior.
Run the test command. The test MUST fail.
## Phase 3: Fix
Make the minimal code change required to make the failing test pass.
Run the full test suite. All tests MUST pass.
## Phase 4: Pull Request
Commit with `fix: (#{{issueNumber}})`.
Push the branch to the user's fork.
Open a PR via `gh pr create` with reproduction + verification output.
```
Because the workflow is markdown, you can tighten it (add a "no `--force` push" rule), loosen it (allow multi-file fixes), or fork it for a different language without touching TypeScript.
#### The Role
`.flue/roles/test-driven-developer.md` defines the agent's persona: a disciplined contributor who treats the target repository as someone else's project. The role is referenced in the skill call (`role: 'test-driven-developer'`) and shapes how the agent makes tradeoffs (minimal change, match host code style, never disable existing tests).
#### How `bug-fix.ts` is actually invoked
Nothing in our code calls our agent's default export directly; Flue's CLI does. Here's the full chain from `npm run dev` to `handler(ctx)`:
**Build time (`flue dev` startup):**
1. `flue dev --target node` calls `dev()` from `@flue/sdk`, which runs `build()`.
2. `build()` does `fs.readdirSync('.flue/agents')` and keeps any entry matching `/\.(ts|js|mts|mjs)$/`. Our `bug-fix.ts` matches → agent name is `bug-fix` (filename without extension).
3. For each agent file, Flue uses the TypeScript AST to find the static `export const triggers = {...}` declaration, validating that `webhook` is `true` or `false`. Our `triggers = { webhook: true }` registers the agent for HTTP access.
4. The build generates a [Hono](https://hono.dev/) server entry that imports each agent's default export, then esbuilds it to `dist/server.mjs`.
5. The dev server spawns `node dist/server.mjs` with `PORT=3583` and `FLUE_MODE=local`.
**Request time (when you `curl`):**
The generated server mounts a single dynamic route, `POST /agents/:name/:id`. When a request arrives:
1. Validate the method, agent name, and webhook accessibility.
2. Parse the JSON body → `payload` (defaults to `{}` for empty bodies).
3. **Pick a response mode based on headers:**
| Headers sent by client | Server behavior | Status |
|---|---|---|
| `Content-Type: application/json` (default) | Wait for handler, return `{ "result": }` | `200` |
| `Accept: text/event-stream` | Stream SSE events (channel names are `tool_start`, `text_delta`, …, finally `result`) | `200` |
| `x-webhook: true` | Fire-and-forget; run handler in background | `202` |
The CLI's pretty-print form (`[flue] tool:start`, `[flue] tool:done`) you see in `flue run` output is `flue run`'s own decoration; the underlying SSE channel names use underscores (`tool_start`, `tool_end`). If you're consuming the SSE stream from your own client, listen for the underscore form.
4. Construct a `FlueContext` (`{ id, payload, env, init }`) and invoke our default export: `handler(ctx)`.
5. Return whatever the handler resolves with, in whichever mode was selected.
So when you run our `curl` example without special headers, you hit the **sync mode**: the connection stays open until the agent finishes (PR opened), then the server returns `{ "result": { branch, prUrl, ... } }`.
If you want to watch the agent's progress live, switch to SSE:
```bash
curl -N -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{}'
```
Or use Flue's one-shot CLI invoker, which handles SSE for you and prints the result to stdout:
```bash
npm run run
# equivalent to:
# flue run bug-fix --target node --id run-1 --env .env --payload '{}'
```
`flue run` builds, spawns the server, POSTs with `Accept: text/event-stream`, streams events to stderr, prints the final result to stdout, and shuts the server down. Perfect for CI.
#### What `id` actually means: sessions
The `` segment in `POST /agents/bug-fix/` is not just a label. It identifies a Flue **session**: the persistent message history and conversation metadata that `agent.session()` opens inside your handler.
```
POST /agents/bug-fix/run-1 ← = "run-1" → session "run-1" for the bug-fix agent
POST /agents/bug-fix/run-2 ← different → fresh session, no shared state
POST /agents/bug-fix/run-1 ← same as before → REUSES session "run-1"
```
**Same `` reused, what actually happens:**
1. Your handler function runs **from the top, every time** (it's just a function, not auto-resumed).
2. `client.create()` makes a **new** Daytona sandbox each call (because our code calls it unconditionally).
3. But `await agent.session()` inside the handler resolves to the **same Flue session object** as the previous call with that id, so the LLM sees the previous run's message history as context for this run.
So same-id reuse persists the **conversation**, not the **sandbox**. For a chat-style agent that's exactly what you want; for our one-shot bug-fix agent, it's mostly noise (the LLM might short-circuit with "I already analyzed this") and ends up confusing things.
**Practical guidance for this guide**:
- One unique `` per run (`run-1`, `run-2`, or `$(uuidgen)`). Treat each invocation as fresh.
- Pick a stable `` only if you want resumability (e.g., agent crashed mid-fix and you want the LLM to remember its prior reasoning). You'd also need to extend the orchestrator to skip sandbox setup when a sandbox already exists for that id.
### 4. Example Walkthrough
Let's trace what happens when you trigger the agent against [`vercel/ms`](https://github.com/vercel/ms) issue [#284](https://github.com/vercel/ms/issues/284). The reporter found that `ms()` violates its own roundtrip contract: `ms(Number.MAX_VALUE)` returns a string in scientific notation that `parse()` can no longer read back.
To watch the agent work tool-by-tool, use Flue's one-shot CLI invoker (already wired into our `package.json`):
```bash
npm run run
```
That runs `flue run bug-fix --target node --id run-1 --env .env --payload '{}'`, which builds, spawns an ephemeral server, POSTs with `Accept: text/event-stream`, and decorates each agent event into a readable progress line. Below is a trimmed real run against a fork of `vercel/ms`. The orchestrator's `[bug-fix] ...` lines come from our setup phase; the `[flue] tool:start/done ...` lines and inline italicized text are the LLM's tool calls and reasoning streamed back via SSE:
```
[bug-fix] target: your-username/your-fork#284 (model: anthropic/claude-sonnet-4-6)
[bug-fix] sandbox ready (id: b5e8e152-7c67-4176-8721-c37aab6aa331)
[bug-fix] installing gh CLI in sandbox...
[bug-fix] commits will be authored as Your Name <12345+your-username@users.noreply.github.com>
[bug-fix] cloning your-username/your-fork into sandbox...
[bug-fix] detected package manager: pnpm
[bug-fix] installing pnpm...
[bug-fix] installing project dependencies...
[bug-fix] resolving issue source: vercel/ms
[bug-fix] fetching issue #284 from vercel/ms...
[bug-fix] uploading skill into sandbox + excluding it from git...
[bug-fix] running TDD workflow (reproduce → fix → PR)...
[flue] Agent started
I'll work through this systematically following the test-driven approach.
[flue] tool:start read /home/daytona/project/package.json
[flue] tool:done read (1477 chars)
[flue] tool:start glob **/*.test.*
[flue] tool:done glob
[flue] tool:start read /home/daytona/project/src/index.ts
[flue] tool:start read /home/daytona/project/src/parse.test.ts
[flue] tool:done read (5864 chars)
[flue] tool:done read (5082 chars)
Now I have a clear picture. Let me verify what `format(Number.MAX_VALUE)` actually
produces, then set up the branch and write the failing test:
[flue] tool:start bash $ node -e "const out = ms(Number.MAX_VALUE); ..."
[flue] tool:done bash (218 chars)
I've confirmed the bug: format(Number.MAX_VALUE) produces "5.696545792019405e+297y"
which the regex in parse() cannot match. Setting up the branch and writing the test:
[flue] tool:start bash $ git checkout -b flue/fix-issue-284
[flue] tool:done bash
[flue] tool:start edit /home/daytona/project/src/parse.test.ts
[flue] tool:done edit
[flue] tool:start bash $ pnpm test:nodejs
[flue] tool:done bash (2102 chars)
The tests fail as expected. Now let me implement the fix:
[flue] tool:start edit /home/daytona/project/src/index.ts
[flue] tool:done edit
[flue] tool:start bash $ pnpm test
[flue] tool:done bash (1319 chars)
All 172 tests pass (167 pre-existing + 5 new). Now let's commit and push:
[flue] tool:start bash $ git add src/index.ts src/parse.test.ts && git commit -m "fix: ..."
[flue] tool:done bash
[flue] tool:start bash $ git push origin flue/fix-issue-284
[flue] tool:done bash
[flue] tool:start bash $ gh pr create --repo your-username/your-fork --base main \
--head flue/fix-issue-284 --title "fix: ..." --body "Closes #284 ..."
[flue] tool:done bash
[bug-fix] PR opened: https://github.com/your-username/your-fork/pull/1
[bug-fix] branch: flue/fix-issue-284
[bug-fix] files changed: src/index.ts, src/parse.test.ts
[bug-fix] tearing down agents + sandbox...
```
Reading top to bottom you can see the agent following our SKILL.md phases: it understands the project (multiple parallel `read` calls), confirms the bug interactively before changing anything (the `node -e` reproduction in `bash`), creates a branch + writes the failing test, runs the suite to confirm the test fails, makes the fix, reruns the suite, then commits, pushes, and opens the PR. The final `[bug-fix] tearing down agents + sandbox...` line is the orchestrator's `try/finally` doing its work — both agents get destroyed and the Daytona sandbox is deleted before the response is returned.
Note that exact wording, file paths, test counts, and PR numbers vary between runs (the LLM is non-deterministic, and the PR number depends on how many PRs your fork already has). The shape — sandbox provision → setup → four-phase TDD workflow → PR URL → cleanup — is what's deterministic.
The four phases below zoom in on each step.
#### Phase 1: Understand
The agent reads `package.json` to identify the test runner (Jest, run via `pnpm test`), then reads the single-file source at `src/index.ts` (244 lines) and an existing parse test (`src/parse.test.ts`) to learn the project's assertion style.
The relevant code is the `parse()` regex around line 77 of `src/index.ts`:
```typescript
const match = /^(?-?\d*\.?\d+) *(?...)?$/i.exec(str);
```
The `value` group only matches plain decimal numbers; it doesn't accept scientific notation (`e+297`). That's the bug.
#### Phase 2: Reproduce
The agent creates a new branch and writes a single, focused test that asserts the **roundtrip property** the reporter described:
```typescript
import { ms } from './index';
describe('issue #284: roundtrip with very large numbers', () => {
it('format() output is always parseable back to a number', () => {
const out = ms(Number.MAX_VALUE);
expect(typeof out).toBe('string');
expect(ms(out)).not.toBeNaN();
});
});
```
It runs `pnpm test` and confirms the failures with the current implementation. From a real run:
```
FAIL src/parse.test.ts
● parse(scientific notation) › should parse scientific notation values with a unit (roundtrip with format)
Expected: false
Received: true (Number.isNaN was true — parse returned NaN)
● parse(scientific notation) › should parse scientific notation with y unit
● parse(scientific notation) › should parse scientific notation with ms unit
● parse(scientific notation) › should parse scientific notation with s unit
● parse(scientific notation) › should parse negative scientific notation with a unit
```
If the tests had unexpectedly passed, the agent would refuse to continue: a test that doesn't fail isn't a reproduction.
#### Phase 3: Fix
With the bug reproduced, the agent makes a minimal, surgical change to `src/index.ts` so `parse()` accepts scientific notation in the numeric value group, then reruns the full suite:
```
PASS src/parse.test.ts
PASS src/index.test.ts
PASS src/format.test.ts
PASS src/parse-strict.test.ts
Test Suites: 4 passed, 4 total
Tests: 172 passed, 172 total
```
Both the new tests and every pre-existing test pass. The fix is a small regex extension (one optional capture group): exactly the kind of minimal change the `test-driven-developer` role rewards.
#### Phase 4: Pull Request
The agent commits, pushes, and opens a PR against your fork:
```bash
$ git commit -m "fix: support scientific notation in parse() to fix roundtrip with large numbers (#284)"
$ git push origin flue/fix-issue-284
$ gh pr create --repo your-username/your-fork \
--base main \
--head flue/fix-issue-284 \
--title "fix: support scientific notation in parse() to fix roundtrip with large numbers (#284)" \
--body "Closes vercel#284 ..."
```
The PR body the agent generated includes the failing-test output from Phase 2, a one-paragraph root-cause analysis, and the passing-test output from Phase 3 — everything a human reviewer needs to merge in under five minutes.
The HTTP response body returned to your `curl` wraps the handler's return value under a `result` key:
```json
{
"result": {
"branch": "flue/fix-issue-284",
"prUrl": "https://github.com/your-username/your-fork/pull/1",
"testFile": "src/parse.test.ts",
"filesChanged": ["src/index.ts", "src/parse.test.ts"],
"summary": "The parse() regex only matched plain decimal numbers in the value group (`-?\\d*\\.?\\d+`), so when format() produced scientific notation (e.g. `5.696545792019405e+297y`) via JavaScript's default number serialisation for very large Math.round() results, parse() returned NaN; the fix extends the value capture group with an optional exponent part (`(?:e[+-]?\\d+)?`) so scientific notation is accepted transparently."
}
}
```
Open the PR URL in your browser to review the diff and merge, exactly as you would for a human-authored contribution.
:::tip[Commit attribution]
Both the commit and the PR are authored under the GitHub account that owns your `GITHUB_TOKEN`. The agent calls `gh api user` at startup to resolve your login + numeric ID, then sets `git config user.email` to the GitHub-recommended `+@users.noreply.github.com` noreply format. GitHub recognizes that email and attaches the commit to your profile (avatar and all). The PR body still has a small `Generated by a Flue + Daytona bug-fix agent.` footer for transparency — if you'd rather drop it (some maintainers prefer not to have third-party tags on contributions), edit the PR-body template at the bottom of `.agents/skills/bug-fix/SKILL.md` and remove that line.
:::
### 5. Cleanup
Flue does **not** auto-destroy sessions when a handler returns — sessions persist for resumability via the same ``, and the `cleanup: true` callback registered on our connector only fires when `agent.destroy()` is explicitly called. The orchestrator therefore wraps the entire two-agent flow in a `try { ... } finally { ... }` block:
```ts
try {
setupAgent = await init({ sandbox: daytona(sandbox, { cleanup: true }), ... })
// ... setup work + projectAgent + skill invocation
return result
} finally {
console.log('[bug-fix] tearing down agents + sandbox...')
if (projectAgent) {
try { await projectAgent.destroy() } catch (err) { console.error(err) }
}
if (setupAgent) {
try { await setupAgent.destroy() } catch (err) { console.error(err) }
} else {
// setupAgent never armed cleanup: true; delete sandbox directly
try { await sandbox.delete() } catch (err) { console.error(err) }
}
}
```
Order matters: the **project** agent is destroyed first (closes its session, no sandbox impact since it doesn't have `cleanup: true`), then the **setup** agent's destroy fires the registered `sandbox.delete()` callback. The fallback `else` branch handles the case where `init()` itself threw before `setupAgent` was created — in that scenario nothing armed `cleanup: true`, so the orchestrator calls `sandbox.delete()` directly on the already-created sandbox. (If `client.create()` itself fails earlier, no sandbox object exists at all, so there's nothing to leak.)
This covers the common failure paths: successful runs, caught LLM errors, and exceptions thrown during the workflow. Cleanup is best-effort — if `destroy()` or `sandbox.delete()` itself throws (transient API error, network drop), the failure is logged but the sandbox is not retried. Confirm in your [Daytona Dashboard](https://app.daytona.io/dashboard) after each run, and clean up any orphans manually if you spot them.
**Key Advantages**
- **TDD by construction**: the skill's phase ordering forces a failing test before any fix lands, so every PR is reproducible.
- **Real pull requests**: the agent uses `gh` inside the sandbox to push and open PRs you can merge in the GitHub UI, not just patches you copy by hand.
- **Skill-first design**: tweak the workflow by editing markdown. No recompile, no redeploy.
- **Structured outputs**: results are validated by Valibot schemas, so downstream automation never has to parse free-form text.
- **Sandbox-isolated execution**: cloning, dependency installation, and test runs all happen inside Daytona, so your host stays clean even if the target repo is malicious or its dependencies are.
# Build a Generative-UI Coding Agent with CopilotKit and Daytona
import { Image } from 'astro:assets'
import copilotkitSnakeGame from '../../../../../assets/docs/images/copilotkit_snake_game.gif'
This guide demonstrates how to build a [CopilotKit](https://docs.showcase.copilotkit.ai/) Built-in Agent backed by a [Daytona](https://www.daytona.io/) sandbox with full shell and filesystem access. The agent handles whatever a developer might do at a terminal: build apps, debug or analyze code, run scripts, work with data, install packages.
Every tool call streams into the chat as generative UI: shell commands render as terminal cards, file edits as syntax-highlighted code, listings and grep results as structured cards, and any hosted process (dev server, static site, API) as a live `