From 4e22f89c91f765f48cc2b348f139f356ec887461 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 17:51:10 -0700
Subject: [PATCH 1/7] feat(integrations): add Daytona integration with sandbox
lifecycle, code execution, and file tools
---
apps/docs/components/icons.tsx | 49 ++
apps/docs/components/ui/icon-mapping.ts | 2 +
.../content/docs/en/integrations/daytona.mdx | 301 ++++++++
.../content/docs/en/integrations/meta.json | 1 +
.../sim/app/api/tools/daytona/upload/route.ts | 118 ++++
apps/sim/blocks/blocks/daytona.ts | 665 ++++++++++++++++++
apps/sim/blocks/registry.ts | 3 +
apps/sim/components/icons.tsx | 49 ++
apps/sim/lib/api/contracts/tools/daytona.ts | 27 +
apps/sim/lib/api/contracts/tools/index.ts | 1 +
apps/sim/lib/integrations/icon-mapping.ts | 2 +
apps/sim/tools/daytona/create_sandbox.ts | 165 +++++
apps/sim/tools/daytona/delete_sandbox.ts | 63 ++
apps/sim/tools/daytona/download_file.ts | 79 +++
apps/sim/tools/daytona/execute_command.ts | 95 +++
apps/sim/tools/daytona/get_sandbox.ts | 60 ++
apps/sim/tools/daytona/git_clone.ts | 99 +++
apps/sim/tools/daytona/index.ts | 12 +
apps/sim/tools/daytona/list_files.ts | 85 +++
apps/sim/tools/daytona/list_sandboxes.ts | 105 +++
apps/sim/tools/daytona/run_code.ts | 95 +++
apps/sim/tools/daytona/start_sandbox.ts | 63 ++
apps/sim/tools/daytona/stop_sandbox.ts | 61 ++
apps/sim/tools/daytona/types.ts | 165 +++++
apps/sim/tools/daytona/upload_file.ts | 90 +++
apps/sim/tools/daytona/utils.ts | 99 +++
apps/sim/tools/registry.ts | 26 +
27 files changed, 2580 insertions(+)
create mode 100644 apps/docs/content/docs/en/integrations/daytona.mdx
create mode 100644 apps/sim/app/api/tools/daytona/upload/route.ts
create mode 100644 apps/sim/blocks/blocks/daytona.ts
create mode 100644 apps/sim/lib/api/contracts/tools/daytona.ts
create mode 100644 apps/sim/tools/daytona/create_sandbox.ts
create mode 100644 apps/sim/tools/daytona/delete_sandbox.ts
create mode 100644 apps/sim/tools/daytona/download_file.ts
create mode 100644 apps/sim/tools/daytona/execute_command.ts
create mode 100644 apps/sim/tools/daytona/get_sandbox.ts
create mode 100644 apps/sim/tools/daytona/git_clone.ts
create mode 100644 apps/sim/tools/daytona/index.ts
create mode 100644 apps/sim/tools/daytona/list_files.ts
create mode 100644 apps/sim/tools/daytona/list_sandboxes.ts
create mode 100644 apps/sim/tools/daytona/run_code.ts
create mode 100644 apps/sim/tools/daytona/start_sandbox.ts
create mode 100644 apps/sim/tools/daytona/stop_sandbox.ts
create mode 100644 apps/sim/tools/daytona/types.ts
create mode 100644 apps/sim/tools/daytona/upload_file.ts
create mode 100644 apps/sim/tools/daytona/utils.ts
diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx
index 2dd06da0b5..23d411b218 100644
--- a/apps/docs/components/icons.tsx
+++ b/apps/docs/components/icons.tsx
@@ -5968,6 +5968,55 @@ export function DatadogIcon(props: SVGProps) {
)
}
+export function DaytonaIcon(props: SVGProps) {
+ return (
+
+ )
+}
+
export function MicrosoftDataverseIcon(props: SVGProps) {
const id = useId()
const clip0 = `dataverse_clip0_${id}`
diff --git a/apps/docs/components/ui/icon-mapping.ts b/apps/docs/components/ui/icon-mapping.ts
index 34c30727ee..c222a25e0e 100644
--- a/apps/docs/components/ui/icon-mapping.ts
+++ b/apps/docs/components/ui/icon-mapping.ts
@@ -44,6 +44,7 @@ import {
DagsterIcon,
DatabricksIcon,
DatadogIcon,
+ DaytonaIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
@@ -268,6 +269,7 @@ export const blockTypeToIconMap: Record = {
dagster: DagsterIcon,
databricks: DatabricksIcon,
datadog: DatadogIcon,
+ daytona: DaytonaIcon,
devin: DevinIcon,
discord: DiscordIcon,
docusign: DocuSignIcon,
diff --git a/apps/docs/content/docs/en/integrations/daytona.mdx b/apps/docs/content/docs/en/integrations/daytona.mdx
new file mode 100644
index 0000000000..054c420a73
--- /dev/null
+++ b/apps/docs/content/docs/en/integrations/daytona.mdx
@@ -0,0 +1,301 @@
+---
+title: Daytona
+description: Run code and commands in secure cloud sandboxes
+---
+
+import { BlockInfoCard } from "@/components/ui/block-info-card"
+
+
+
+{/* MANUAL-CONTENT-START:intro */}
+[Daytona](https://www.daytona.io/) is secure, elastic infrastructure for running AI-generated code. Daytona provides isolated sandboxes that spin up in milliseconds, giving your agents a safe place to execute shell commands, run code, work with files, and clone repositories — without ever touching your own machines.
+
+**Why Daytona?**
+- **Built for AI-generated code:** Sandboxes are fully isolated runtimes, so untrusted or generated code can run safely with no risk to your infrastructure.
+- **Fast, elastic sandboxes:** Create a sandbox in under a couple hundred milliseconds, use it for one task or keep it alive across a whole session, and let auto-stop and auto-delete intervals clean up after you.
+- **Complete toolbox:** Execute shell commands, run Python, JavaScript, or TypeScript with a built-in code interpreter, transfer files in and out, and clone Git repositories — all through one API.
+- **Programmatic lifecycle control:** Create, list, start, stop, and delete sandboxes on demand, with snapshots, regions, resource sizing, environment variables, and labels.
+
+**Using Daytona in Sim**
+
+Sim's Daytona integration connects your workflows to Daytona with an API key. Twelve operations cover the full sandbox lifecycle and toolbox: create, list, get, start, stop, and delete sandboxes; run code and execute commands inside them; upload, download, and list files; and clone Git repositories.
+
+**Key benefits of using Daytona in Sim:**
+- **Safe code interpreter for agents:** Let an agent write Python, JavaScript, or TypeScript and execute it in an isolated sandbox, then use the output downstream in your workflow.
+- **Real file handling:** Upload workflow files directly into a sandbox, process them with code or commands, and download the results as files other blocks can consume.
+- **Repository automation:** Clone a repository into a sandbox, run installs, builds, or tests, and report the results — perfect for CI-style checks and repo health monitoring.
+- **Cost-aware lifecycle:** Create sandboxes on demand, stop or delete them when work finishes, and use auto-stop intervals so idle sandboxes never run up your bill.
+
+Whether you're building an AI code interpreter, validating generated code before it ships, analyzing data files in a clean environment, or automating repository checks, Daytona in Sim gives your agents real compute with strong isolation. Configure your API key, pick an operation, and start running code.
+{/* MANUAL-CONTENT-END */}
+
+
+## Usage Instructions
+
+Integrate Daytona into your workflow to run AI-generated code in secure, isolated sandboxes. Create and manage sandboxes, execute shell commands, run Python, JavaScript, or TypeScript code, transfer files, and clone Git repositories.
+
+
+
+## Actions
+
+### `daytona_create_sandbox`
+
+Create a new Daytona sandbox for running AI-generated code in isolation
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `snapshot` | string | No | ID or name of the snapshot to create the sandbox from \(uses default if empty\) |
+| `name` | string | No | Name for the sandbox \(defaults to the sandbox ID\) |
+| `target` | string | No | Region where the sandbox will be created \(e.g., us, eu\) |
+| `user` | string | No | User associated with the sandbox |
+| `env` | json | No | Environment variables to set in the sandbox as key-value pairs |
+| `labels` | json | No | Labels to attach to the sandbox as key-value pairs |
+| `cpu` | number | No | CPU cores to allocate to the sandbox |
+| `memory` | number | No | Memory to allocate to the sandbox in GB |
+| `disk` | number | No | Disk space to allocate to the sandbox in GB |
+| `autoStopInterval` | number | No | Auto-stop interval in minutes \(0 disables auto-stop\) |
+| `autoArchiveInterval` | number | No | Auto-archive interval in minutes \(0 uses the maximum interval\) |
+| `autoDeleteInterval` | number | No | Auto-delete interval in minutes \(negative disables, 0 deletes immediately on stop\) |
+| `public` | boolean | No | Whether the sandbox HTTP preview is publicly accessible |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandbox` | json | The created sandbox |
+
+### `daytona_list_sandboxes`
+
+List Daytona sandboxes in the organization
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `limit` | number | No | Maximum number of sandboxes to return |
+| `name` | string | No | Filter sandboxes by name prefix \(case-insensitive\) |
+| `labels` | json | No | Filter sandboxes by labels as key-value pairs |
+| `cursor` | string | No | Pagination cursor from a previous response |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandboxes` | array | Sandboxes in the organization |
+| `nextCursor` | string | Cursor for the next page of results |
+
+### `daytona_get_sandbox`
+
+Get details of a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID or name of the sandbox |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandbox` | json | The sandbox details |
+
+### `daytona_start_sandbox`
+
+Start a stopped Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID or name of the sandbox |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandbox` | json | The started sandbox |
+
+### `daytona_stop_sandbox`
+
+Stop a running Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID or name of the sandbox |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandbox` | json | The stopped sandbox |
+
+### `daytona_delete_sandbox`
+
+Delete a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID or name of the sandbox |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `sandbox` | json | The deleted sandbox |
+
+### `daytona_execute_command`
+
+Execute a shell command inside a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to execute the command in |
+| `command` | string | Yes | Shell command to execute |
+| `cwd` | string | No | Working directory for the command \(defaults to the sandbox working directory\) |
+| `env` | json | No | Environment variables to set for the command as key-value pairs |
+| `timeout` | number | No | Timeout in seconds \(defaults to 10 seconds\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `exitCode` | number | Exit code of the command |
+| `result` | string | Combined stdout/stderr output of the command |
+
+### `daytona_run_code`
+
+Run Python, JavaScript, or TypeScript code inside a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to run the code in |
+| `code` | string | Yes | Code to run |
+| `language` | string | Yes | Language of the code: python, javascript, or typescript |
+| `env` | json | No | Environment variables to set for the run as key-value pairs |
+| `timeout` | number | No | Timeout in seconds \(defaults to 10 seconds\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `exitCode` | number | Exit code of the code run |
+| `result` | string | Combined stdout/stderr output of the code run |
+| `artifacts` | json | Artifacts produced by the run \(e.g., matplotlib charts\) |
+
+### `daytona_upload_file`
+
+Upload a file to a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to upload the file to |
+| `destinationPath` | string | Yes | Destination path in the sandbox \(a trailing slash uploads into that directory using the file name\) |
+| `file` | file | No | The file to upload |
+| `fileContent` | string | No | Legacy: base64 encoded file content |
+| `fileName` | string | No | Optional file name override |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `uploadedPath` | string | Path of the uploaded file in the sandbox |
+| `name` | string | Name of the uploaded file |
+| `size` | number | Size of the uploaded file in bytes |
+
+### `daytona_download_file`
+
+Download a file from a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to download the file from |
+| `filePath` | string | Yes | Path of the file in the sandbox |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `file` | file | Downloaded file stored in execution files |
+| `name` | string | Name of the downloaded file |
+| `mimeType` | string | MIME type of the downloaded file |
+| `size` | number | Size of the downloaded file in bytes |
+
+### `daytona_list_files`
+
+List files in a directory of a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to list files in |
+| `path` | string | No | Directory path to list \(defaults to the sandbox working directory\) |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `files` | array | Files and directories at the given path |
+| ↳ `name` | string | File or directory name |
+| ↳ `isDir` | boolean | Whether the entry is a directory |
+| ↳ `size` | number | Size in bytes |
+| ↳ `mode` | string | File mode string |
+| ↳ `permissions` | string | Permission string |
+| ↳ `owner` | string | Owning user |
+| ↳ `group` | string | Owning group |
+| ↳ `modifiedAt` | string | Last modification timestamp |
+
+### `daytona_git_clone`
+
+Clone a Git repository into a Daytona sandbox
+
+#### Input
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `apiKey` | string | Yes | Daytona API key |
+| `sandboxId` | string | Yes | ID of the sandbox to clone the repository into |
+| `url` | string | Yes | URL of the Git repository to clone |
+| `path` | string | Yes | Path in the sandbox to clone the repository into |
+| `branch` | string | No | Branch to clone \(defaults to the default branch\) |
+| `commitId` | string | No | Specific commit to check out after cloning |
+| `username` | string | No | Username for authenticating to private repositories |
+| `password` | string | No | Password or personal access token for private repositories |
+
+#### Output
+
+| Parameter | Type | Description |
+| --------- | ---- | ----------- |
+| `repoUrl` | string | URL of the cloned repository |
+| `clonePath` | string | Path the repository was cloned into |
+
+
diff --git a/apps/docs/content/docs/en/integrations/meta.json b/apps/docs/content/docs/en/integrations/meta.json
index ca880ca7bb..0ee13b9b63 100644
--- a/apps/docs/content/docs/en/integrations/meta.json
+++ b/apps/docs/content/docs/en/integrations/meta.json
@@ -41,6 +41,7 @@
"dagster",
"databricks",
"datadog",
+ "daytona",
"devin",
"discord",
"docusign",
diff --git a/apps/sim/app/api/tools/daytona/upload/route.ts b/apps/sim/app/api/tools/daytona/upload/route.ts
new file mode 100644
index 0000000000..a5dc58651e
--- /dev/null
+++ b/apps/sim/app/api/tools/daytona/upload/route.ts
@@ -0,0 +1,118 @@
+import { createLogger } from '@sim/logger'
+import { getErrorMessage } from '@sim/utils/errors'
+import { type NextRequest, NextResponse } from 'next/server'
+import { daytonaUploadFileContract } from '@/lib/api/contracts/tools/daytona'
+import { parseRequest } from '@/lib/api/server'
+import { checkInternalAuth } from '@/lib/auth/hybrid'
+import { generateRequestId } from '@/lib/core/utils/request'
+import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
+import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils'
+import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
+import { assertToolFileAccess } from '@/app/api/files/authorization'
+import { DAYTONA_TOOLBOX_BASE_URL, extractDaytonaError } from '@/tools/daytona/utils'
+
+export const dynamic = 'force-dynamic'
+
+const logger = createLogger('DaytonaUploadAPI')
+
+const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024
+
+export const POST = withRouteHandler(async (request: NextRequest) => {
+ const requestId = generateRequestId()
+
+ try {
+ const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
+
+ if (!authResult.success || !authResult.userId) {
+ logger.warn(`[${requestId}] Unauthorized Daytona upload attempt: ${authResult.error}`)
+ return NextResponse.json(
+ { success: false, error: authResult.error || 'Authentication required' },
+ { status: 401 }
+ )
+ }
+
+ logger.info(`[${requestId}] Authenticated Daytona upload request via ${authResult.authType}`)
+
+ const parsed = await parseRequest(daytonaUploadFileContract, request, {})
+ if (!parsed.success) return parsed.response
+ const params = parsed.data.body
+
+ let fileBuffer: Buffer
+ let fileName: string
+
+ if (params.file) {
+ const userFiles = processFilesToUserFiles([params.file as RawFileInput], requestId, logger)
+
+ if (userFiles.length === 0) {
+ return NextResponse.json({ success: false, error: 'Invalid file input' }, { status: 400 })
+ }
+
+ const userFile = userFiles[0]
+ const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
+ if (denied) return denied
+
+ logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
+ fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
+ fileName = params.fileName || userFile.name
+ } else if (params.fileContent) {
+ logger.info(`[${requestId}] Using legacy base64 content input`)
+ fileBuffer = Buffer.from(params.fileContent, 'base64')
+ fileName = params.fileName || 'file'
+ } else {
+ return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
+ }
+
+ if (fileBuffer.length > MAX_UPLOAD_SIZE_BYTES) {
+ const sizeMB = (fileBuffer.length / (1024 * 1024)).toFixed(2)
+ return NextResponse.json(
+ { success: false, error: `File size (${sizeMB}MB) exceeds upload limit of 100MB` },
+ { status: 400 }
+ )
+ }
+
+ const destinationPath = params.destinationPath.endsWith('/')
+ ? `${params.destinationPath}${fileName}`
+ : params.destinationPath
+
+ logger.info(
+ `[${requestId}] Uploading to Daytona sandbox ${params.sandboxId}: ${destinationPath} (${fileBuffer.length} bytes)`
+ )
+
+ const formData = new FormData()
+ formData.append(
+ 'file',
+ new Blob([new Uint8Array(fileBuffer)], { type: 'application/octet-stream' }),
+ fileName
+ )
+
+ const uploadUrl = `${DAYTONA_TOOLBOX_BASE_URL}/${encodeURIComponent(params.sandboxId.trim())}/files/upload?path=${encodeURIComponent(destinationPath)}`
+ const response = await fetch(uploadUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${params.apiKey}`,
+ },
+ body: formData,
+ })
+
+ if (!response.ok) {
+ const errorMessage = await extractDaytonaError(response, 'Failed to upload file')
+ logger.error(`[${requestId}] Daytona API error:`, { status: response.status, errorMessage })
+ return NextResponse.json({ success: false, error: errorMessage }, { status: response.status })
+ }
+
+ logger.info(`[${requestId}] File uploaded successfully: ${destinationPath}`)
+
+ return NextResponse.json({
+ success: true,
+ uploadedPath: destinationPath,
+ name: fileName,
+ size: fileBuffer.length,
+ })
+ } catch (error) {
+ logger.error(`[${requestId}] Unexpected error:`, error)
+ return NextResponse.json(
+ { success: false, error: getErrorMessage(error, 'Unknown error') },
+ { status: 500 }
+ )
+ }
+})
diff --git a/apps/sim/blocks/blocks/daytona.ts b/apps/sim/blocks/blocks/daytona.ts
new file mode 100644
index 0000000000..8f5757c51d
--- /dev/null
+++ b/apps/sim/blocks/blocks/daytona.ts
@@ -0,0 +1,665 @@
+import { DaytonaIcon } from '@/components/icons'
+import type { BlockConfig, BlockMeta } from '@/blocks/types'
+import { AuthMode, IntegrationType } from '@/blocks/types'
+import { normalizeFileInput } from '@/blocks/utils'
+
+const SANDBOX_SCOPED_OPERATIONS = [
+ 'execute_command',
+ 'run_code',
+ 'upload_file',
+ 'download_file',
+ 'list_files',
+ 'git_clone',
+ 'get_sandbox',
+ 'start_sandbox',
+ 'stop_sandbox',
+ 'delete_sandbox',
+]
+
+export const DaytonaBlock: BlockConfig = {
+ type: 'daytona',
+ name: 'Daytona',
+ description: 'Run code and commands in secure cloud sandboxes',
+ longDescription:
+ 'Integrate Daytona into your workflow to run AI-generated code in secure, isolated sandboxes. Create and manage sandboxes, execute shell commands, run Python, JavaScript, or TypeScript code, transfer files, and clone Git repositories.',
+ docsLink: 'https://docs.sim.ai/integrations/daytona',
+ category: 'tools',
+ integrationType: IntegrationType.DevOps,
+ bgColor: '#FFFFFF',
+ icon: DaytonaIcon,
+ authMode: AuthMode.ApiKey,
+
+ subBlocks: [
+ {
+ id: 'operation',
+ title: 'Operation',
+ type: 'dropdown',
+ options: [
+ { label: 'Create Sandbox', id: 'create_sandbox' },
+ { label: 'Run Code', id: 'run_code' },
+ { label: 'Execute Command', id: 'execute_command' },
+ { label: 'Upload File', id: 'upload_file' },
+ { label: 'Download File', id: 'download_file' },
+ { label: 'List Files', id: 'list_files' },
+ { label: 'Git Clone', id: 'git_clone' },
+ { label: 'List Sandboxes', id: 'list_sandboxes' },
+ { label: 'Get Sandbox', id: 'get_sandbox' },
+ { label: 'Start Sandbox', id: 'start_sandbox' },
+ { label: 'Stop Sandbox', id: 'stop_sandbox' },
+ { label: 'Delete Sandbox', id: 'delete_sandbox' },
+ ],
+ value: () => 'create_sandbox',
+ },
+ {
+ id: 'apiKey',
+ title: 'API Key',
+ type: 'short-input',
+ placeholder: 'Enter your Daytona API key',
+ password: true,
+ required: true,
+ },
+ {
+ id: 'sandboxId',
+ title: 'Sandbox ID',
+ type: 'short-input',
+ placeholder: 'ID or name of the sandbox',
+ condition: { field: 'operation', value: SANDBOX_SCOPED_OPERATIONS },
+ required: { field: 'operation', value: SANDBOX_SCOPED_OPERATIONS },
+ },
+
+ // Create Sandbox fields
+ {
+ id: 'snapshot',
+ title: 'Snapshot',
+ type: 'short-input',
+ placeholder: 'Snapshot ID or name (uses default if empty)',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'sandboxName',
+ title: 'Sandbox Name',
+ type: 'short-input',
+ placeholder: 'Name for the sandbox',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'envVars',
+ title: 'Environment Variables',
+ type: 'table',
+ columns: ['Key', 'Value'],
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'sandboxLabels',
+ title: 'Labels',
+ type: 'table',
+ columns: ['Key', 'Value'],
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'target',
+ title: 'Region',
+ type: 'short-input',
+ placeholder: 'Region for the sandbox (e.g., us, eu)',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'sandboxUser',
+ title: 'User',
+ type: 'short-input',
+ placeholder: 'User associated with the sandbox',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'cpu',
+ title: 'CPU Cores',
+ type: 'short-input',
+ placeholder: 'CPU cores to allocate',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'memory',
+ title: 'Memory (GB)',
+ type: 'short-input',
+ placeholder: 'Memory to allocate in GB',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'disk',
+ title: 'Disk (GB)',
+ type: 'short-input',
+ placeholder: 'Disk space to allocate in GB',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'autoStopInterval',
+ title: 'Auto-Stop Interval (minutes)',
+ type: 'short-input',
+ placeholder: 'Auto-stop interval in minutes (0 disables)',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'autoArchiveInterval',
+ title: 'Auto-Archive Interval (minutes)',
+ type: 'short-input',
+ placeholder: 'Auto-archive interval in minutes',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'autoDeleteInterval',
+ title: 'Auto-Delete Interval (minutes)',
+ type: 'short-input',
+ placeholder: 'Auto-delete interval in minutes (negative disables)',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+ {
+ id: 'isPublic',
+ title: 'Public Preview',
+ type: 'switch',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'create_sandbox' },
+ },
+
+ // Run Code fields
+ {
+ id: 'language',
+ title: 'Language',
+ type: 'dropdown',
+ options: [
+ { label: 'Python', id: 'python' },
+ { label: 'JavaScript', id: 'javascript' },
+ { label: 'TypeScript', id: 'typescript' },
+ ],
+ value: () => 'python',
+ condition: { field: 'operation', value: 'run_code' },
+ },
+ {
+ id: 'code',
+ title: 'Code',
+ type: 'code',
+ placeholder: 'Code to run in the sandbox',
+ condition: { field: 'operation', value: 'run_code' },
+ required: { field: 'operation', value: 'run_code' },
+ wandConfig: {
+ enabled: true,
+ prompt:
+ 'Generate code to run inside an isolated Daytona sandbox in the selected language (Python, JavaScript, or TypeScript). The code runs as a standalone script; print results to stdout. Return ONLY the code without any markdown formatting or explanation.',
+ placeholder: 'Describe the code you want to run',
+ },
+ },
+
+ // Execute Command fields
+ {
+ id: 'command',
+ title: 'Command',
+ type: 'long-input',
+ placeholder: 'Shell command to execute (e.g., ls -la)',
+ rows: 3,
+ condition: { field: 'operation', value: 'execute_command' },
+ required: { field: 'operation', value: 'execute_command' },
+ },
+ {
+ id: 'cwd',
+ title: 'Working Directory',
+ type: 'short-input',
+ placeholder: 'Working directory for the command',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'execute_command' },
+ },
+
+ // Shared Run Code / Execute Command fields
+ {
+ id: 'runEnv',
+ title: 'Environment Variables',
+ type: 'table',
+ columns: ['Key', 'Value'],
+ mode: 'advanced',
+ condition: { field: 'operation', value: ['execute_command', 'run_code'] },
+ },
+ {
+ id: 'timeout',
+ title: 'Timeout (seconds)',
+ type: 'short-input',
+ placeholder: 'Timeout in seconds (defaults to 10)',
+ mode: 'advanced',
+ condition: { field: 'operation', value: ['execute_command', 'run_code'] },
+ },
+
+ // Upload File fields
+ {
+ id: 'uploadFile',
+ title: 'File',
+ type: 'file-upload',
+ canonicalParamId: 'file',
+ placeholder: 'Upload file to send to the sandbox',
+ mode: 'basic',
+ multiple: false,
+ condition: { field: 'operation', value: 'upload_file' },
+ required: { field: 'operation', value: 'upload_file' },
+ },
+ {
+ id: 'fileRef',
+ title: 'File',
+ type: 'short-input',
+ canonicalParamId: 'file',
+ placeholder: 'Reference file from previous blocks',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'upload_file' },
+ required: { field: 'operation', value: 'upload_file' },
+ },
+ {
+ id: 'destinationPath',
+ title: 'Destination Path',
+ type: 'short-input',
+ placeholder: 'Path in the sandbox (trailing slash uploads into directory)',
+ condition: { field: 'operation', value: 'upload_file' },
+ required: { field: 'operation', value: 'upload_file' },
+ },
+ {
+ id: 'uploadFileName',
+ title: 'File Name',
+ type: 'short-input',
+ placeholder: 'Optional file name override',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'upload_file' },
+ },
+
+ // Download File fields
+ {
+ id: 'filePath',
+ title: 'File Path',
+ type: 'short-input',
+ placeholder: 'Path of the file in the sandbox',
+ condition: { field: 'operation', value: 'download_file' },
+ required: { field: 'operation', value: 'download_file' },
+ },
+
+ // List Files fields
+ {
+ id: 'directoryPath',
+ title: 'Directory Path',
+ type: 'short-input',
+ placeholder: 'Directory to list (defaults to working directory)',
+ condition: { field: 'operation', value: 'list_files' },
+ },
+
+ // Git Clone fields
+ {
+ id: 'repoUrl',
+ title: 'Repository URL',
+ type: 'short-input',
+ placeholder: 'https://github.com/org/repo.git',
+ condition: { field: 'operation', value: 'git_clone' },
+ required: { field: 'operation', value: 'git_clone' },
+ },
+ {
+ id: 'clonePath',
+ title: 'Clone Path',
+ type: 'short-input',
+ placeholder: 'Path in the sandbox to clone into',
+ condition: { field: 'operation', value: 'git_clone' },
+ required: { field: 'operation', value: 'git_clone' },
+ },
+ {
+ id: 'gitBranch',
+ title: 'Branch',
+ type: 'short-input',
+ placeholder: 'Branch to clone (defaults to the default branch)',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'git_clone' },
+ },
+ {
+ id: 'gitCommitId',
+ title: 'Commit',
+ type: 'short-input',
+ placeholder: 'Specific commit to check out',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'git_clone' },
+ },
+ {
+ id: 'gitUsername',
+ title: 'Git Username',
+ type: 'short-input',
+ placeholder: 'Username for private repositories',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'git_clone' },
+ },
+ {
+ id: 'gitPassword',
+ title: 'Git Password / Token',
+ type: 'short-input',
+ placeholder: 'Password or access token for private repositories',
+ password: true,
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'git_clone' },
+ },
+
+ // List Sandboxes fields
+ {
+ id: 'nameFilter',
+ title: 'Name Filter',
+ type: 'short-input',
+ placeholder: 'Filter sandboxes by name prefix',
+ condition: { field: 'operation', value: 'list_sandboxes' },
+ },
+ {
+ id: 'limit',
+ title: 'Limit',
+ type: 'short-input',
+ placeholder: 'Maximum number of sandboxes to return',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'list_sandboxes' },
+ },
+ {
+ id: 'labelFilter',
+ title: 'Label Filter',
+ type: 'table',
+ columns: ['Key', 'Value'],
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'list_sandboxes' },
+ },
+ {
+ id: 'cursor',
+ title: 'Cursor',
+ type: 'short-input',
+ placeholder: 'Pagination cursor from a previous response',
+ mode: 'advanced',
+ condition: { field: 'operation', value: 'list_sandboxes' },
+ },
+ ],
+
+ tools: {
+ access: [
+ 'daytona_create_sandbox',
+ 'daytona_list_sandboxes',
+ 'daytona_get_sandbox',
+ 'daytona_start_sandbox',
+ 'daytona_stop_sandbox',
+ 'daytona_delete_sandbox',
+ 'daytona_execute_command',
+ 'daytona_run_code',
+ 'daytona_upload_file',
+ 'daytona_download_file',
+ 'daytona_list_files',
+ 'daytona_git_clone',
+ ],
+ config: {
+ tool: (params) => `daytona_${params.operation}`,
+ params: (params) => {
+ const { operation, apiKey, ...rest } = params
+
+ const baseParams: Record = { apiKey }
+
+ if (SANDBOX_SCOPED_OPERATIONS.includes(operation)) {
+ baseParams.sandboxId = rest.sandboxId
+ }
+
+ switch (operation) {
+ case 'create_sandbox':
+ if (rest.snapshot) baseParams.snapshot = rest.snapshot
+ if (rest.sandboxName) baseParams.name = rest.sandboxName
+ if (rest.target) baseParams.target = rest.target
+ if (rest.sandboxUser) baseParams.user = rest.sandboxUser
+ if (rest.envVars) baseParams.env = rest.envVars
+ if (rest.sandboxLabels) baseParams.labels = rest.sandboxLabels
+ if (rest.cpu) baseParams.cpu = Number(rest.cpu)
+ if (rest.memory) baseParams.memory = Number(rest.memory)
+ if (rest.disk) baseParams.disk = Number(rest.disk)
+ if (rest.autoStopInterval !== undefined && rest.autoStopInterval !== '') {
+ baseParams.autoStopInterval = Number(rest.autoStopInterval)
+ }
+ if (rest.autoArchiveInterval !== undefined && rest.autoArchiveInterval !== '') {
+ baseParams.autoArchiveInterval = Number(rest.autoArchiveInterval)
+ }
+ if (rest.autoDeleteInterval !== undefined && rest.autoDeleteInterval !== '') {
+ baseParams.autoDeleteInterval = Number(rest.autoDeleteInterval)
+ }
+ if (rest.isPublic != null) baseParams.public = rest.isPublic
+ break
+ case 'run_code':
+ baseParams.code = rest.code
+ baseParams.language = rest.language
+ if (rest.runEnv) baseParams.env = rest.runEnv
+ if (rest.timeout) baseParams.timeout = Number(rest.timeout)
+ break
+ case 'execute_command':
+ baseParams.command = rest.command
+ if (rest.cwd) baseParams.cwd = rest.cwd
+ if (rest.runEnv) baseParams.env = rest.runEnv
+ if (rest.timeout) baseParams.timeout = Number(rest.timeout)
+ break
+ case 'upload_file': {
+ const normalizedFile = normalizeFileInput(rest.file, { single: true })
+ if (normalizedFile) baseParams.file = normalizedFile
+ baseParams.destinationPath = rest.destinationPath
+ if (rest.uploadFileName) baseParams.fileName = rest.uploadFileName
+ break
+ }
+ case 'download_file':
+ baseParams.filePath = rest.filePath
+ break
+ case 'list_files':
+ if (rest.directoryPath) baseParams.path = rest.directoryPath
+ break
+ case 'git_clone':
+ baseParams.url = rest.repoUrl
+ baseParams.path = rest.clonePath
+ if (rest.gitBranch) baseParams.branch = rest.gitBranch
+ if (rest.gitCommitId) baseParams.commitId = rest.gitCommitId
+ if (rest.gitUsername) baseParams.username = rest.gitUsername
+ if (rest.gitPassword) baseParams.password = rest.gitPassword
+ break
+ case 'list_sandboxes':
+ if (rest.nameFilter) baseParams.name = rest.nameFilter
+ if (rest.limit) baseParams.limit = Number(rest.limit)
+ if (rest.labelFilter) baseParams.labels = rest.labelFilter
+ if (rest.cursor) baseParams.cursor = rest.cursor
+ break
+ }
+
+ return baseParams
+ },
+ },
+ },
+
+ inputs: {
+ operation: { type: 'string', description: 'Operation to perform' },
+ apiKey: { type: 'string', description: 'Daytona API key' },
+ sandboxId: { type: 'string', description: 'ID or name of the sandbox' },
+ snapshot: { type: 'string', description: 'Snapshot to create the sandbox from' },
+ sandboxName: { type: 'string', description: 'Name for the sandbox' },
+ target: { type: 'string', description: 'Region for the sandbox' },
+ sandboxUser: { type: 'string', description: 'User associated with the sandbox' },
+ envVars: { type: 'json', description: 'Environment variables for the sandbox' },
+ sandboxLabels: { type: 'json', description: 'Labels for the sandbox' },
+ cpu: { type: 'number', description: 'CPU cores to allocate' },
+ memory: { type: 'number', description: 'Memory to allocate in GB' },
+ disk: { type: 'number', description: 'Disk space to allocate in GB' },
+ autoStopInterval: { type: 'number', description: 'Auto-stop interval in minutes' },
+ autoArchiveInterval: { type: 'number', description: 'Auto-archive interval in minutes' },
+ autoDeleteInterval: { type: 'number', description: 'Auto-delete interval in minutes' },
+ isPublic: { type: 'boolean', description: 'Whether the HTTP preview is public' },
+ language: { type: 'string', description: 'Language of the code to run' },
+ code: { type: 'string', description: 'Code to run in the sandbox' },
+ command: { type: 'string', description: 'Shell command to execute' },
+ cwd: { type: 'string', description: 'Working directory for the command' },
+ runEnv: { type: 'json', description: 'Environment variables for the run' },
+ timeout: { type: 'number', description: 'Timeout in seconds' },
+ file: { type: 'json', description: 'File to upload to the sandbox' },
+ destinationPath: { type: 'string', description: 'Destination path in the sandbox' },
+ uploadFileName: { type: 'string', description: 'Optional file name override' },
+ filePath: { type: 'string', description: 'Path of the file to download' },
+ directoryPath: { type: 'string', description: 'Directory to list' },
+ repoUrl: { type: 'string', description: 'URL of the Git repository' },
+ clonePath: { type: 'string', description: 'Path to clone the repository into' },
+ gitBranch: { type: 'string', description: 'Branch to clone' },
+ gitCommitId: { type: 'string', description: 'Commit to check out' },
+ gitUsername: { type: 'string', description: 'Username for private repositories' },
+ gitPassword: { type: 'string', description: 'Password or token for private repositories' },
+ nameFilter: { type: 'string', description: 'Name prefix filter for sandboxes' },
+ limit: { type: 'number', description: 'Maximum number of sandboxes to return' },
+ labelFilter: { type: 'json', description: 'Label filter for sandboxes' },
+ cursor: { type: 'string', description: 'Pagination cursor from a previous response' },
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'Sandbox details (create, get, start, stop, delete operations)',
+ },
+ sandboxes: { type: 'json', description: 'Sandboxes list (list sandboxes operation)' },
+ nextCursor: {
+ type: 'string',
+ description: 'Cursor for the next page (list sandboxes operation)',
+ },
+ exitCode: {
+ type: 'number',
+ description: 'Exit code (execute command and run code operations)',
+ },
+ result: {
+ type: 'string',
+ description: 'Combined stdout/stderr output (execute command and run code operations)',
+ },
+ artifacts: { type: 'json', description: 'Run artifacts such as charts (run code operation)' },
+ uploadedPath: {
+ type: 'string',
+ description: 'Path of the uploaded file (upload file operation)',
+ },
+ file: { type: 'file', description: 'Downloaded file (download file operation)' },
+ name: {
+ type: 'string',
+ description: 'File name (upload file and download file operations)',
+ },
+ mimeType: { type: 'string', description: 'MIME type (download file operation)' },
+ size: {
+ type: 'number',
+ description: 'File size in bytes (upload file and download file operations)',
+ },
+ files: { type: 'json', description: 'Files at the given path (list files operation)' },
+ repoUrl: { type: 'string', description: 'Cloned repository URL (git clone operation)' },
+ clonePath: { type: 'string', description: 'Clone destination path (git clone operation)' },
+ },
+}
+
+export const DaytonaBlockMeta = {
+ tags: ['agentic', 'cloud', 'automation'],
+ templates: [
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona code interpreter',
+ prompt:
+ 'Build a workflow where an agent answers data questions by writing Python, creating a Daytona sandbox, running the code in it, and replying with the computed result and any printed output.',
+ modules: ['agent', 'workflows'],
+ category: 'engineering',
+ tags: ['automation', 'code-execution'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona PR test runner',
+ prompt:
+ 'Create a workflow triggered by a GitHub pull request that creates a Daytona sandbox, clones the repository at the PR branch, runs the test suite with an execute command, and posts the pass/fail summary back to the PR.',
+ modules: ['agent', 'workflows'],
+ category: 'engineering',
+ tags: ['ci', 'automation'],
+ alsoIntegrations: ['github'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona CSV analysis',
+ prompt:
+ 'Build a workflow that takes an uploaded CSV file, uploads it into a Daytona sandbox, runs a Python analysis script over it, downloads the generated report file, and shares the findings in Slack.',
+ modules: ['files', 'agent', 'workflows'],
+ category: 'engineering',
+ tags: ['data', 'automation'],
+ alsoIntegrations: ['slack'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona scheduled script runner',
+ prompt:
+ 'Create a scheduled daily workflow that spins up a Daytona sandbox, runs a maintenance script with an execute command, writes the output to a results table, and deletes the sandbox when finished.',
+ modules: ['scheduled', 'tables', 'workflows'],
+ category: 'operations',
+ tags: ['automation', 'maintenance'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona sandbox janitor',
+ prompt:
+ 'Build a scheduled weekly workflow that lists all Daytona sandboxes, stops any that have been running longer than expected, deletes sandboxes labeled as temporary, and posts a cleanup summary to Slack.',
+ modules: ['scheduled', 'agent', 'workflows'],
+ category: 'operations',
+ tags: ['automation', 'cost-control'],
+ alsoIntegrations: ['slack'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona generated-code validator',
+ prompt:
+ 'Create a workflow where an agent generates code from a user request, runs it in a Daytona sandbox, inspects the exit code and output, fixes errors and reruns until the code passes, then returns the validated code.',
+ modules: ['agent', 'workflows'],
+ category: 'engineering',
+ tags: ['automation', 'code-execution'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona repo health check',
+ prompt:
+ 'Build a scheduled workflow that creates a Daytona sandbox, clones the main branch of a repository, runs install and build commands, records any failures in a table, and alerts the engineering channel when the build breaks.',
+ modules: ['scheduled', 'tables', 'workflows'],
+ category: 'engineering',
+ tags: ['ci', 'monitoring'],
+ alsoIntegrations: ['slack'],
+ },
+ {
+ icon: DaytonaIcon,
+ title: 'Daytona file transformer',
+ prompt:
+ 'Create a workflow that receives a document from a form, uploads it to a Daytona sandbox, runs a conversion script on it, downloads the transformed file, and emails it back to the requester.',
+ modules: ['files', 'workflows'],
+ category: 'operations',
+ tags: ['automation', 'documents'],
+ alsoIntegrations: ['gmail'],
+ },
+ ],
+ skills: [
+ {
+ name: 'run-code-in-sandbox',
+ description:
+ 'Run Python, JavaScript, or TypeScript in an isolated Daytona sandbox and return the output. Use as a safe code interpreter for computation, parsing, or data wrangling.',
+ content:
+ '# Run Code In Sandbox\n\nExecute code safely in an isolated Daytona sandbox.\n\n## Steps\n1. Create a sandbox with the Create Sandbox operation (or reuse an existing sandbox ID), setting an auto-stop interval so it cleans up after itself.\n2. Use Run Code with the language set to python, javascript, or typescript. Print the values you need to stdout — the printed output is what comes back.\n3. Check the exit code: 0 means success; on failure, read the result output for the error, fix the code, and rerun.\n4. Delete the sandbox with Delete Sandbox when the work is finished if it was created just for this task.\n\n## Output\nReturn the printed result and the exit code. If the run produced chart artifacts, mention them. On repeated failures, return the last error output instead of fabricating a result.',
+ },
+ {
+ name: 'analyze-data-file',
+ description:
+ 'Upload a data file into a Daytona sandbox, analyze it with Python, and download any generated results. Use for CSV/JSON analysis that needs real computation.',
+ content:
+ '# Analyze Data File\n\nProcess a workflow file with real code in a sandbox.\n\n## Steps\n1. Create a sandbox (or reuse one) and note its ID.\n2. Use Upload File to place the data file at a known path, e.g. /home/daytona/data.csv. A trailing slash on the destination uploads into that directory under the original file name.\n3. Use Run Code with Python to read the file from that path, compute the analysis, print a summary, and write any derived files (reports, charts) to known paths.\n4. Use Download File to pull each derived file back into the workflow, and List Files to discover outputs if paths are dynamic.\n5. Delete the sandbox when finished.\n\n## Output\nReturn the printed analysis summary and the downloaded result files. Name the exact sandbox paths used so the run can be reproduced.',
+ },
+ {
+ name: 'clone-repo-and-run-checks',
+ description:
+ 'Clone a Git repository into a Daytona sandbox and run install, build, or test commands. Use for CI-style checks, repo health monitoring, or validating changes.',
+ content:
+ '# Clone Repo And Run Checks\n\nRun repository checks in an isolated environment.\n\n## Steps\n1. Create a sandbox sized for the job (set CPU and memory in advanced options for heavy builds).\n2. Use Git Clone with the repository URL and a clone path like /home/daytona/repo. Pass a branch for non-default branches, and a username plus token for private repositories.\n3. Use Execute Command from the clone path (set the working directory) to run installs, builds, or tests. Raise the timeout for long commands — the default is 10 seconds.\n4. Inspect each exit code and capture the output of failing commands.\n5. Delete the sandbox when the checks complete.\n\n## Output\nReturn pass/fail per command with exit codes, and include the failing output verbatim when something breaks.',
+ },
+ {
+ name: 'manage-sandbox-fleet',
+ description:
+ 'List, stop, and delete Daytona sandboxes to control cost and tidy up environments. Use for scheduled cleanup or auditing what is currently running.',
+ content:
+ '# Manage Sandbox Fleet\n\nAudit and clean up sandboxes in the organization.\n\n## Steps\n1. Use List Sandboxes to enumerate sandboxes; filter by name prefix or labels, and page with the cursor when there are many.\n2. Inspect each sandbox state and timestamps to find ones idle or running longer than expected.\n3. Stop Sandbox for environments worth keeping but not actively used; Delete Sandbox for disposable ones (e.g. labeled temporary).\n4. When creating sandboxes elsewhere, set auto-stop and auto-delete intervals so cleanup happens automatically.\n\n## Output\nReturn a summary of sandboxes found, which were stopped or deleted and why, and any that were left running with their states.',
+ },
+ ],
+} as const satisfies BlockMeta
diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts
index bb3fd882b8..efc25757b3 100644
--- a/apps/sim/blocks/registry.ts
+++ b/apps/sim/blocks/registry.ts
@@ -45,6 +45,7 @@ import { CursorBlock, CursorBlockMeta, CursorV2Block } from '@/blocks/blocks/cur
import { DagsterBlock, DagsterBlockMeta } from '@/blocks/blocks/dagster'
import { DatabricksBlock, DatabricksBlockMeta } from '@/blocks/blocks/databricks'
import { DatadogBlock, DatadogBlockMeta } from '@/blocks/blocks/datadog'
+import { DaytonaBlock, DaytonaBlockMeta } from '@/blocks/blocks/daytona'
import { DevinBlock, DevinBlockMeta } from '@/blocks/blocks/devin'
import { DiscordBlock, DiscordBlockMeta } from '@/blocks/blocks/discord'
import { DocuSignBlock, DocuSignBlockMeta } from '@/blocks/blocks/docusign'
@@ -373,6 +374,7 @@ const BLOCK_REGISTRY: Record = {
dagster: DagsterBlock,
databricks: DatabricksBlock,
datadog: DatadogBlock,
+ daytona: DaytonaBlock,
devin: DevinBlock,
discord: DiscordBlock,
docusign: DocuSignBlock,
@@ -667,6 +669,7 @@ const BLOCK_META_REGISTRY: Record = {
dagster: DagsterBlockMeta,
databricks: DatabricksBlockMeta,
datadog: DatadogBlockMeta,
+ daytona: DaytonaBlockMeta,
devin: DevinBlockMeta,
discord: DiscordBlockMeta,
docusign: DocuSignBlockMeta,
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 2dd06da0b5..23d411b218 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -5968,6 +5968,55 @@ export function DatadogIcon(props: SVGProps) {
)
}
+export function DaytonaIcon(props: SVGProps) {
+ return (
+
+ )
+}
+
export function MicrosoftDataverseIcon(props: SVGProps) {
const id = useId()
const clip0 = `dataverse_clip0_${id}`
diff --git a/apps/sim/lib/api/contracts/tools/daytona.ts b/apps/sim/lib/api/contracts/tools/daytona.ts
new file mode 100644
index 0000000000..71ee1ff83c
--- /dev/null
+++ b/apps/sim/lib/api/contracts/tools/daytona.ts
@@ -0,0 +1,27 @@
+import { z } from 'zod'
+import { defineRouteContract } from '@/lib/api/contracts/types'
+import { FileInputSchema } from '@/lib/uploads/utils/file-schemas'
+
+export const daytonaUploadFileBodySchema = z.object({
+ apiKey: z.string().min(1, 'API key is required'),
+ sandboxId: z.string().min(1, 'Sandbox ID is required'),
+ destinationPath: z.string().min(1, 'Destination path is required'),
+ file: FileInputSchema.optional().nullable(),
+ fileContent: z.string().nullish(),
+ fileName: z.string().nullish(),
+})
+
+export const daytonaUploadFileResponseSchema = z.object({
+ success: z.boolean(),
+ uploadedPath: z.string().optional(),
+ name: z.string().optional(),
+ size: z.number().optional(),
+ error: z.string().optional(),
+})
+
+export const daytonaUploadFileContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/tools/daytona/upload',
+ body: daytonaUploadFileBodySchema,
+ response: { mode: 'json', schema: daytonaUploadFileResponseSchema },
+})
diff --git a/apps/sim/lib/api/contracts/tools/index.ts b/apps/sim/lib/api/contracts/tools/index.ts
index 5a09f5095a..eefc2bdc83 100644
--- a/apps/sim/lib/api/contracts/tools/index.ts
+++ b/apps/sim/lib/api/contracts/tools/index.ts
@@ -7,6 +7,7 @@ export * from './crowdstrike'
export * from './cursor'
export * from './custom'
export * from './databases'
+export * from './daytona'
export * from './docusign'
export * from './evernote'
export * from './file'
diff --git a/apps/sim/lib/integrations/icon-mapping.ts b/apps/sim/lib/integrations/icon-mapping.ts
index 6055e1043b..4b71d06bd0 100644
--- a/apps/sim/lib/integrations/icon-mapping.ts
+++ b/apps/sim/lib/integrations/icon-mapping.ts
@@ -44,6 +44,7 @@ import {
DagsterIcon,
DatabricksIcon,
DatadogIcon,
+ DaytonaIcon,
DevinIcon,
DiscordIcon,
DocumentIcon,
@@ -264,6 +265,7 @@ export const blockTypeToIconMap: Record = {
dagster: DagsterIcon,
databricks: DatabricksIcon,
datadog: DatadogIcon,
+ daytona: DaytonaIcon,
devin: DevinIcon,
discord: DiscordIcon,
docusign: DocuSignIcon,
diff --git a/apps/sim/tools/daytona/create_sandbox.ts b/apps/sim/tools/daytona/create_sandbox.ts
new file mode 100644
index 0000000000..acdf5884eb
--- /dev/null
+++ b/apps/sim/tools/daytona/create_sandbox.ts
@@ -0,0 +1,165 @@
+import type { DaytonaCreateSandboxParams, DaytonaSandboxResponse } from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+ toOptionalBoolean,
+ toOptionalNumber,
+} from '@/tools/daytona/utils'
+import { transformTable } from '@/tools/shared/table'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaCreateSandboxTool: ToolConfig<
+ DaytonaCreateSandboxParams,
+ DaytonaSandboxResponse
+> = {
+ id: 'daytona_create_sandbox',
+ name: 'Daytona Create Sandbox',
+ description: 'Create a new Daytona sandbox for running AI-generated code in isolation',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ snapshot: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'ID or name of the snapshot to create the sandbox from (uses default if empty)',
+ },
+ name: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Name for the sandbox (defaults to the sandbox ID)',
+ },
+ target: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Region where the sandbox will be created (e.g., us, eu)',
+ },
+ user: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'User associated with the sandbox',
+ },
+ env: {
+ type: 'json',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Environment variables to set in the sandbox as key-value pairs',
+ },
+ labels: {
+ type: 'json',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Labels to attach to the sandbox as key-value pairs',
+ },
+ cpu: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'CPU cores to allocate to the sandbox',
+ },
+ memory: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Memory to allocate to the sandbox in GB',
+ },
+ disk: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Disk space to allocate to the sandbox in GB',
+ },
+ autoStopInterval: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Auto-stop interval in minutes (0 disables auto-stop)',
+ },
+ autoArchiveInterval: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Auto-archive interval in minutes (0 uses the maximum interval)',
+ },
+ autoDeleteInterval: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description:
+ 'Auto-delete interval in minutes (negative disables, 0 deletes immediately on stop)',
+ },
+ public: {
+ type: 'boolean',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Whether the sandbox HTTP preview is publicly accessible',
+ },
+ },
+
+ request: {
+ url: `${DAYTONA_API_BASE_URL}/sandbox`,
+ method: 'POST',
+ headers: (params) => ({
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ body: (params) => {
+ const body: Record = {}
+ if (params.snapshot) body.snapshot = params.snapshot
+ if (params.name) body.name = params.name
+ if (params.target) body.target = params.target
+ if (params.user) body.user = params.user
+ const env = transformTable(params.env ?? null)
+ if (Object.keys(env).length > 0) body.env = env
+ const labels = transformTable(params.labels ?? null)
+ if (Object.keys(labels).length > 0) body.labels = labels
+ const cpu = toOptionalNumber(params.cpu)
+ if (cpu !== undefined) body.cpu = cpu
+ const memory = toOptionalNumber(params.memory)
+ if (memory !== undefined) body.memory = memory
+ const disk = toOptionalNumber(params.disk)
+ if (disk !== undefined) body.disk = disk
+ const autoStopInterval = toOptionalNumber(params.autoStopInterval)
+ if (autoStopInterval !== undefined) body.autoStopInterval = autoStopInterval
+ const autoArchiveInterval = toOptionalNumber(params.autoArchiveInterval)
+ if (autoArchiveInterval !== undefined) body.autoArchiveInterval = autoArchiveInterval
+ const autoDeleteInterval = toOptionalNumber(params.autoDeleteInterval)
+ if (autoDeleteInterval !== undefined) body.autoDeleteInterval = autoDeleteInterval
+ const isPublic = toOptionalBoolean(params.public)
+ if (isPublic !== undefined) body.public = isPublic
+ return body
+ },
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to create sandbox'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ sandbox: mapDaytonaSandbox(data),
+ },
+ }
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'The created sandbox',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/delete_sandbox.ts b/apps/sim/tools/daytona/delete_sandbox.ts
new file mode 100644
index 0000000000..9c622f863e
--- /dev/null
+++ b/apps/sim/tools/daytona/delete_sandbox.ts
@@ -0,0 +1,63 @@
+import type { DaytonaDeleteSandboxParams, DaytonaSandboxResponse } from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+} from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaDeleteSandboxTool: ToolConfig<
+ DaytonaDeleteSandboxParams,
+ DaytonaSandboxResponse
+> = {
+ id: 'daytona_delete_sandbox',
+ name: 'Daytona Delete Sandbox',
+ description: 'Delete a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID or name of the sandbox',
+ },
+ },
+
+ request: {
+ url: (params) =>
+ `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}`,
+ method: 'DELETE',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to delete sandbox'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ sandbox: mapDaytonaSandbox(data),
+ },
+ }
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'The deleted sandbox',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/download_file.ts b/apps/sim/tools/daytona/download_file.ts
new file mode 100644
index 0000000000..efccd8331c
--- /dev/null
+++ b/apps/sim/tools/daytona/download_file.ts
@@ -0,0 +1,79 @@
+import type { DaytonaDownloadFileParams, DaytonaDownloadFileResponse } from '@/tools/daytona/types'
+import { daytonaToolboxUrl, extractDaytonaError } from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaDownloadFileTool: ToolConfig<
+ DaytonaDownloadFileParams,
+ DaytonaDownloadFileResponse
+> = {
+ id: 'daytona_download_file',
+ name: 'Daytona Download File',
+ description: 'Download a file from a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to download the file from',
+ },
+ filePath: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Path of the file in the sandbox',
+ },
+ },
+
+ request: {
+ url: (params) =>
+ daytonaToolboxUrl(
+ params.sandboxId,
+ `/files/download?path=${encodeURIComponent(params.filePath.trim())}`
+ ),
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response, params) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to download file'))
+ }
+
+ const mimeType = response.headers.get('content-type') || 'application/octet-stream'
+ const fileName = params?.filePath.trim().split('/').filter(Boolean).pop() || 'download'
+ const arrayBuffer = await response.arrayBuffer()
+ const buffer = Buffer.from(arrayBuffer)
+
+ return {
+ success: true,
+ output: {
+ file: {
+ name: fileName,
+ mimeType,
+ data: buffer.toString('base64'),
+ size: buffer.length,
+ },
+ name: fileName,
+ mimeType,
+ size: buffer.length,
+ },
+ }
+ },
+
+ outputs: {
+ file: { type: 'file', description: 'Downloaded file stored in execution files' },
+ name: { type: 'string', description: 'Name of the downloaded file' },
+ mimeType: { type: 'string', description: 'MIME type of the downloaded file' },
+ size: { type: 'number', description: 'Size of the downloaded file in bytes' },
+ },
+}
diff --git a/apps/sim/tools/daytona/execute_command.ts b/apps/sim/tools/daytona/execute_command.ts
new file mode 100644
index 0000000000..be35c18b1f
--- /dev/null
+++ b/apps/sim/tools/daytona/execute_command.ts
@@ -0,0 +1,95 @@
+import type {
+ DaytonaExecuteCommandParams,
+ DaytonaExecuteCommandResponse,
+} from '@/tools/daytona/types'
+import { daytonaToolboxUrl, extractDaytonaError, toOptionalNumber } from '@/tools/daytona/utils'
+import { transformTable } from '@/tools/shared/table'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaExecuteCommandTool: ToolConfig<
+ DaytonaExecuteCommandParams,
+ DaytonaExecuteCommandResponse
+> = {
+ id: 'daytona_execute_command',
+ name: 'Daytona Execute Command',
+ description: 'Execute a shell command inside a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to execute the command in',
+ },
+ command: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Shell command to execute',
+ },
+ cwd: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Working directory for the command (defaults to the sandbox working directory)',
+ },
+ env: {
+ type: 'json',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Environment variables to set for the command as key-value pairs',
+ },
+ timeout: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Timeout in seconds (defaults to 10 seconds)',
+ },
+ },
+
+ request: {
+ url: (params) => daytonaToolboxUrl(params.sandboxId, '/process/execute'),
+ method: 'POST',
+ headers: (params) => ({
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ body: (params) => {
+ const body: Record = {
+ command: params.command,
+ }
+ if (params.cwd) body.cwd = params.cwd
+ const envs = transformTable(params.env ?? null)
+ if (Object.keys(envs).length > 0) body.envs = envs
+ const timeout = toOptionalNumber(params.timeout)
+ if (timeout !== undefined) body.timeout = timeout
+ return body
+ },
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to execute command'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ exitCode: data.exitCode ?? 0,
+ result: data.result ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ exitCode: { type: 'number', description: 'Exit code of the command' },
+ result: { type: 'string', description: 'Combined stdout/stderr output of the command' },
+ },
+}
diff --git a/apps/sim/tools/daytona/get_sandbox.ts b/apps/sim/tools/daytona/get_sandbox.ts
new file mode 100644
index 0000000000..2b6c59efea
--- /dev/null
+++ b/apps/sim/tools/daytona/get_sandbox.ts
@@ -0,0 +1,60 @@
+import type { DaytonaGetSandboxParams, DaytonaSandboxResponse } from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+} from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaGetSandboxTool: ToolConfig = {
+ id: 'daytona_get_sandbox',
+ name: 'Daytona Get Sandbox',
+ description: 'Get details of a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID or name of the sandbox',
+ },
+ },
+
+ request: {
+ url: (params) =>
+ `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}`,
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to get sandbox'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ sandbox: mapDaytonaSandbox(data),
+ },
+ }
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'The sandbox details',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/git_clone.ts b/apps/sim/tools/daytona/git_clone.ts
new file mode 100644
index 0000000000..d2a43c0713
--- /dev/null
+++ b/apps/sim/tools/daytona/git_clone.ts
@@ -0,0 +1,99 @@
+import type { DaytonaGitCloneParams, DaytonaGitCloneResponse } from '@/tools/daytona/types'
+import { daytonaToolboxUrl, extractDaytonaError } from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaGitCloneTool: ToolConfig = {
+ id: 'daytona_git_clone',
+ name: 'Daytona Git Clone',
+ description: 'Clone a Git repository into a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to clone the repository into',
+ },
+ url: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'URL of the Git repository to clone',
+ },
+ path: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Path in the sandbox to clone the repository into',
+ },
+ branch: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Branch to clone (defaults to the default branch)',
+ },
+ commitId: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Specific commit to check out after cloning',
+ },
+ username: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Username for authenticating to private repositories',
+ },
+ password: {
+ type: 'string',
+ required: false,
+ visibility: 'user-only',
+ description: 'Password or personal access token for private repositories',
+ },
+ },
+
+ request: {
+ url: (params) => daytonaToolboxUrl(params.sandboxId, '/git/clone'),
+ method: 'POST',
+ headers: (params) => ({
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ body: (params) => {
+ const body: Record = {
+ url: params.url,
+ path: params.path,
+ }
+ if (params.branch) body.branch = params.branch
+ if (params.commitId) body.commit_id = params.commitId
+ if (params.username) body.username = params.username
+ if (params.password) body.password = params.password
+ return body
+ },
+ },
+
+ transformResponse: async (response, params) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to clone repository'))
+ }
+ return {
+ success: true,
+ output: {
+ repoUrl: params?.url ?? '',
+ clonePath: params?.path ?? '',
+ },
+ }
+ },
+
+ outputs: {
+ repoUrl: { type: 'string', description: 'URL of the cloned repository' },
+ clonePath: { type: 'string', description: 'Path the repository was cloned into' },
+ },
+}
diff --git a/apps/sim/tools/daytona/index.ts b/apps/sim/tools/daytona/index.ts
new file mode 100644
index 0000000000..9c567c905a
--- /dev/null
+++ b/apps/sim/tools/daytona/index.ts
@@ -0,0 +1,12 @@
+export { daytonaCreateSandboxTool } from '@/tools/daytona/create_sandbox'
+export { daytonaDeleteSandboxTool } from '@/tools/daytona/delete_sandbox'
+export { daytonaDownloadFileTool } from '@/tools/daytona/download_file'
+export { daytonaExecuteCommandTool } from '@/tools/daytona/execute_command'
+export { daytonaGetSandboxTool } from '@/tools/daytona/get_sandbox'
+export { daytonaGitCloneTool } from '@/tools/daytona/git_clone'
+export { daytonaListFilesTool } from '@/tools/daytona/list_files'
+export { daytonaListSandboxesTool } from '@/tools/daytona/list_sandboxes'
+export { daytonaRunCodeTool } from '@/tools/daytona/run_code'
+export { daytonaStartSandboxTool } from '@/tools/daytona/start_sandbox'
+export { daytonaStopSandboxTool } from '@/tools/daytona/stop_sandbox'
+export { daytonaUploadFileTool } from '@/tools/daytona/upload_file'
diff --git a/apps/sim/tools/daytona/list_files.ts b/apps/sim/tools/daytona/list_files.ts
new file mode 100644
index 0000000000..c753924992
--- /dev/null
+++ b/apps/sim/tools/daytona/list_files.ts
@@ -0,0 +1,85 @@
+import type { DaytonaListFilesParams, DaytonaListFilesResponse } from '@/tools/daytona/types'
+import { daytonaToolboxUrl, extractDaytonaError } from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaListFilesTool: ToolConfig = {
+ id: 'daytona_list_files',
+ name: 'Daytona List Files',
+ description: 'List files in a directory of a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to list files in',
+ },
+ path: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Directory path to list (defaults to the sandbox working directory)',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const query = params.path ? `?path=${encodeURIComponent(params.path.trim())}` : ''
+ return daytonaToolboxUrl(params.sandboxId, `/files${query}`)
+ },
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to list files'))
+ }
+ const data = await response.json()
+ const files = Array.isArray(data) ? data : []
+ return {
+ success: true,
+ output: {
+ files: files.map((file: Record) => ({
+ name: file.name ?? '',
+ isDir: file.isDir ?? false,
+ size: file.size ?? 0,
+ mode: file.mode ?? '',
+ permissions: file.permissions ?? '',
+ owner: file.owner ?? '',
+ group: file.group ?? '',
+ modifiedAt: file.modifiedAt ?? '',
+ })),
+ },
+ }
+ },
+
+ outputs: {
+ files: {
+ type: 'array',
+ description: 'Files and directories at the given path',
+ items: {
+ type: 'json',
+ properties: {
+ name: { type: 'string', description: 'File or directory name' },
+ isDir: { type: 'boolean', description: 'Whether the entry is a directory' },
+ size: { type: 'number', description: 'Size in bytes' },
+ mode: { type: 'string', description: 'File mode string' },
+ permissions: { type: 'string', description: 'Permission string' },
+ owner: { type: 'string', description: 'Owning user' },
+ group: { type: 'string', description: 'Owning group' },
+ modifiedAt: { type: 'string', description: 'Last modification timestamp' },
+ },
+ },
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/list_sandboxes.ts b/apps/sim/tools/daytona/list_sandboxes.ts
new file mode 100644
index 0000000000..044aad49da
--- /dev/null
+++ b/apps/sim/tools/daytona/list_sandboxes.ts
@@ -0,0 +1,105 @@
+import type {
+ DaytonaListSandboxesParams,
+ DaytonaListSandboxesResponse,
+} from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+ toOptionalNumber,
+} from '@/tools/daytona/utils'
+import { transformTable } from '@/tools/shared/table'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaListSandboxesTool: ToolConfig<
+ DaytonaListSandboxesParams,
+ DaytonaListSandboxesResponse
+> = {
+ id: 'daytona_list_sandboxes',
+ name: 'Daytona List Sandboxes',
+ description: 'List Daytona sandboxes in the organization',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ limit: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Maximum number of sandboxes to return',
+ },
+ name: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter sandboxes by name prefix (case-insensitive)',
+ },
+ labels: {
+ type: 'json',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Filter sandboxes by labels as key-value pairs',
+ },
+ cursor: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Pagination cursor from a previous response',
+ },
+ },
+
+ request: {
+ url: (params) => {
+ const query = new URLSearchParams()
+ const limit = toOptionalNumber(params.limit)
+ if (limit !== undefined) query.set('limit', String(limit))
+ if (params.name) query.set('name', params.name)
+ const labels = transformTable(params.labels ?? null)
+ if (Object.keys(labels).length > 0) query.set('labels', JSON.stringify(labels))
+ if (params.cursor) query.set('cursor', params.cursor)
+ const queryString = query.toString()
+ return `${DAYTONA_API_BASE_URL}/sandbox${queryString ? `?${queryString}` : ''}`
+ },
+ method: 'GET',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to list sandboxes'))
+ }
+ const data = await response.json()
+ const items = Array.isArray(data?.items) ? data.items : []
+ return {
+ success: true,
+ output: {
+ sandboxes: items.map(mapDaytonaSandbox),
+ nextCursor: data?.nextCursor ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ sandboxes: {
+ type: 'array',
+ description: 'Sandboxes in the organization',
+ items: {
+ type: 'json',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+ nextCursor: {
+ type: 'string',
+ description: 'Cursor for the next page of results',
+ optional: true,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/run_code.ts b/apps/sim/tools/daytona/run_code.ts
new file mode 100644
index 0000000000..317658307c
--- /dev/null
+++ b/apps/sim/tools/daytona/run_code.ts
@@ -0,0 +1,95 @@
+import type { DaytonaRunCodeParams, DaytonaRunCodeResponse } from '@/tools/daytona/types'
+import { daytonaToolboxUrl, extractDaytonaError, toOptionalNumber } from '@/tools/daytona/utils'
+import { transformTable } from '@/tools/shared/table'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaRunCodeTool: ToolConfig = {
+ id: 'daytona_run_code',
+ name: 'Daytona Run Code',
+ description: 'Run Python, JavaScript, or TypeScript code inside a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to run the code in',
+ },
+ code: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Code to run',
+ },
+ language: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'Language of the code: python, javascript, or typescript',
+ },
+ env: {
+ type: 'json',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Environment variables to set for the run as key-value pairs',
+ },
+ timeout: {
+ type: 'number',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Timeout in seconds (defaults to 10 seconds)',
+ },
+ },
+
+ request: {
+ url: (params) => daytonaToolboxUrl(params.sandboxId, '/process/code-run'),
+ method: 'POST',
+ headers: (params) => ({
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ body: (params) => {
+ const body: Record = {
+ code: params.code,
+ language: params.language,
+ }
+ const envs = transformTable(params.env ?? null)
+ if (Object.keys(envs).length > 0) body.envs = envs
+ const timeout = toOptionalNumber(params.timeout)
+ if (timeout !== undefined) body.timeout = timeout
+ return body
+ },
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to run code'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ exitCode: data.exitCode ?? 0,
+ result: data.result ?? '',
+ artifacts: data.artifacts ?? null,
+ },
+ }
+ },
+
+ outputs: {
+ exitCode: { type: 'number', description: 'Exit code of the code run' },
+ result: { type: 'string', description: 'Combined stdout/stderr output of the code run' },
+ artifacts: {
+ type: 'json',
+ description: 'Artifacts produced by the run (e.g., matplotlib charts)',
+ optional: true,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/start_sandbox.ts b/apps/sim/tools/daytona/start_sandbox.ts
new file mode 100644
index 0000000000..4f369bb27e
--- /dev/null
+++ b/apps/sim/tools/daytona/start_sandbox.ts
@@ -0,0 +1,63 @@
+import type { DaytonaSandboxResponse, DaytonaStartSandboxParams } from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+} from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaStartSandboxTool: ToolConfig<
+ DaytonaStartSandboxParams,
+ DaytonaSandboxResponse
+> = {
+ id: 'daytona_start_sandbox',
+ name: 'Daytona Start Sandbox',
+ description: 'Start a stopped Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID or name of the sandbox',
+ },
+ },
+
+ request: {
+ url: (params) =>
+ `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}/start`,
+ method: 'POST',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to start sandbox'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ sandbox: mapDaytonaSandbox(data),
+ },
+ }
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'The started sandbox',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+}
diff --git a/apps/sim/tools/daytona/stop_sandbox.ts b/apps/sim/tools/daytona/stop_sandbox.ts
new file mode 100644
index 0000000000..15a2a22410
--- /dev/null
+++ b/apps/sim/tools/daytona/stop_sandbox.ts
@@ -0,0 +1,61 @@
+import type { DaytonaSandboxResponse, DaytonaStopSandboxParams } from '@/tools/daytona/types'
+import {
+ DAYTONA_API_BASE_URL,
+ DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ extractDaytonaError,
+ mapDaytonaSandbox,
+} from '@/tools/daytona/utils'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaStopSandboxTool: ToolConfig =
+ {
+ id: 'daytona_stop_sandbox',
+ name: 'Daytona Stop Sandbox',
+ description: 'Stop a running Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID or name of the sandbox',
+ },
+ },
+
+ request: {
+ url: (params) =>
+ `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}/stop`,
+ method: 'POST',
+ headers: (params) => ({
+ Authorization: `Bearer ${params.apiKey}`,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ if (!response.ok) {
+ throw new Error(await extractDaytonaError(response, 'Failed to stop sandbox'))
+ }
+ const data = await response.json()
+ return {
+ success: true,
+ output: {
+ sandbox: mapDaytonaSandbox(data),
+ },
+ }
+ },
+
+ outputs: {
+ sandbox: {
+ type: 'json',
+ description: 'The stopped sandbox',
+ properties: DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ },
+ },
+ }
diff --git a/apps/sim/tools/daytona/types.ts b/apps/sim/tools/daytona/types.ts
new file mode 100644
index 0000000000..491c7dfbff
--- /dev/null
+++ b/apps/sim/tools/daytona/types.ts
@@ -0,0 +1,165 @@
+import type { TableRow, ToolResponse } from '@/tools/types'
+
+export interface DaytonaSandboxSummary {
+ id: string
+ name: string
+ state: string | null
+ snapshot: string | null
+ target: string | null
+ cpu: number | null
+ gpu: number | null
+ memory: number | null
+ disk: number | null
+ labels: Record
+ public: boolean | null
+ errorReason: string | null
+ autoStopInterval: number | null
+ createdAt: string | null
+ updatedAt: string | null
+}
+
+export interface DaytonaFileInfo {
+ name: string
+ isDir: boolean
+ size: number
+ mode: string
+ permissions: string
+ owner: string
+ group: string
+ modifiedAt: string
+}
+
+interface DaytonaBaseParams {
+ apiKey: string
+}
+
+interface DaytonaSandboxScopedParams extends DaytonaBaseParams {
+ sandboxId: string
+}
+
+export interface DaytonaCreateSandboxParams extends DaytonaBaseParams {
+ snapshot?: string
+ name?: string
+ target?: string
+ user?: string
+ env?: TableRow[] | Record | string
+ labels?: TableRow[] | Record | string
+ cpu?: number
+ memory?: number
+ disk?: number
+ autoStopInterval?: number
+ autoArchiveInterval?: number
+ autoDeleteInterval?: number
+ public?: boolean
+}
+
+export interface DaytonaListSandboxesParams extends DaytonaBaseParams {
+ limit?: number
+ name?: string
+ labels?: TableRow[] | Record | string
+ cursor?: string
+}
+
+export type DaytonaGetSandboxParams = DaytonaSandboxScopedParams
+
+export type DaytonaStartSandboxParams = DaytonaSandboxScopedParams
+
+export type DaytonaStopSandboxParams = DaytonaSandboxScopedParams
+
+export type DaytonaDeleteSandboxParams = DaytonaSandboxScopedParams
+
+export interface DaytonaExecuteCommandParams extends DaytonaSandboxScopedParams {
+ command: string
+ cwd?: string
+ env?: TableRow[] | Record | string
+ timeout?: number
+}
+
+export interface DaytonaRunCodeParams extends DaytonaSandboxScopedParams {
+ code: string
+ language: 'python' | 'javascript' | 'typescript'
+ env?: TableRow[] | Record | string
+ timeout?: number
+}
+
+export interface DaytonaUploadFileParams extends DaytonaSandboxScopedParams {
+ destinationPath: string
+ file?: unknown
+ fileContent?: string
+ fileName?: string
+}
+
+export interface DaytonaDownloadFileParams extends DaytonaSandboxScopedParams {
+ filePath: string
+}
+
+export interface DaytonaListFilesParams extends DaytonaSandboxScopedParams {
+ path?: string
+}
+
+export interface DaytonaGitCloneParams extends DaytonaSandboxScopedParams {
+ url: string
+ path: string
+ branch?: string
+ commitId?: string
+ username?: string
+ password?: string
+}
+
+export interface DaytonaSandboxResponse extends ToolResponse {
+ output: {
+ sandbox: DaytonaSandboxSummary
+ }
+}
+
+export interface DaytonaListSandboxesResponse extends ToolResponse {
+ output: {
+ sandboxes: DaytonaSandboxSummary[]
+ nextCursor: string | null
+ }
+}
+
+export interface DaytonaExecuteCommandResponse extends ToolResponse {
+ output: {
+ exitCode: number
+ result: string
+ }
+}
+
+export interface DaytonaRunCodeResponse extends ToolResponse {
+ output: {
+ exitCode: number
+ result: string
+ artifacts: Record | null
+ }
+}
+
+export interface DaytonaUploadFileResponse extends ToolResponse {
+ output: {
+ uploadedPath: string
+ name: string
+ size: number
+ }
+}
+
+export interface DaytonaDownloadFileResponse extends ToolResponse {
+ output: {
+ file: unknown
+ name: string
+ mimeType: string
+ size: number
+ }
+}
+
+export interface DaytonaListFilesResponse extends ToolResponse {
+ output: {
+ files: DaytonaFileInfo[]
+ }
+}
+
+export interface DaytonaGitCloneResponse extends ToolResponse {
+ output: {
+ repoUrl: string
+ clonePath: string
+ }
+}
diff --git a/apps/sim/tools/daytona/upload_file.ts b/apps/sim/tools/daytona/upload_file.ts
new file mode 100644
index 0000000000..943d559c4c
--- /dev/null
+++ b/apps/sim/tools/daytona/upload_file.ts
@@ -0,0 +1,90 @@
+import type { DaytonaUploadFileParams, DaytonaUploadFileResponse } from '@/tools/daytona/types'
+import type { ToolConfig } from '@/tools/types'
+
+export const daytonaUploadFileTool: ToolConfig =
+ {
+ id: 'daytona_upload_file',
+ name: 'Daytona Upload File',
+ description: 'Upload a file to a Daytona sandbox',
+ version: '1.0.0',
+
+ params: {
+ apiKey: {
+ type: 'string',
+ required: true,
+ visibility: 'user-only',
+ description: 'Daytona API key',
+ },
+ sandboxId: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description: 'ID of the sandbox to upload the file to',
+ },
+ destinationPath: {
+ type: 'string',
+ required: true,
+ visibility: 'user-or-llm',
+ description:
+ 'Destination path in the sandbox (a trailing slash uploads into that directory using the file name)',
+ },
+ file: {
+ type: 'file',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'The file to upload',
+ },
+ fileContent: {
+ type: 'string',
+ required: false,
+ visibility: 'hidden',
+ description: 'Legacy: base64 encoded file content',
+ },
+ fileName: {
+ type: 'string',
+ required: false,
+ visibility: 'user-or-llm',
+ description: 'Optional file name override',
+ },
+ },
+
+ request: {
+ url: '/api/tools/daytona/upload',
+ method: 'POST',
+ headers: () => ({
+ 'Content-Type': 'application/json',
+ }),
+ body: (params) => ({
+ apiKey: params.apiKey,
+ sandboxId: params.sandboxId,
+ destinationPath: params.destinationPath,
+ file: params.file,
+ fileContent: params.fileContent,
+ fileName: params.fileName,
+ }),
+ },
+
+ transformResponse: async (response) => {
+ const data = await response.json()
+ if (!response.ok || !data.success) {
+ throw new Error(data.error || 'Failed to upload file')
+ }
+ return {
+ success: true,
+ output: {
+ uploadedPath: data.uploadedPath,
+ name: data.name,
+ size: data.size,
+ },
+ }
+ },
+
+ outputs: {
+ uploadedPath: {
+ type: 'string',
+ description: 'Path of the uploaded file in the sandbox',
+ },
+ name: { type: 'string', description: 'Name of the uploaded file' },
+ size: { type: 'number', description: 'Size of the uploaded file in bytes' },
+ },
+ }
diff --git a/apps/sim/tools/daytona/utils.ts b/apps/sim/tools/daytona/utils.ts
new file mode 100644
index 0000000000..ad2d011ec3
--- /dev/null
+++ b/apps/sim/tools/daytona/utils.ts
@@ -0,0 +1,99 @@
+import type { DaytonaSandboxSummary } from '@/tools/daytona/types'
+
+export const DAYTONA_API_BASE_URL = 'https://app.daytona.io/api'
+
+export const DAYTONA_TOOLBOX_BASE_URL = 'https://proxy.app.daytona.io/toolbox'
+
+/**
+ * Builds a toolbox API URL for a sandbox-scoped endpoint.
+ */
+export function daytonaToolboxUrl(sandboxId: string, path: string): string {
+ return `${DAYTONA_TOOLBOX_BASE_URL}/${encodeURIComponent(sandboxId.trim())}${path}`
+}
+
+/**
+ * Extracts a human-readable error message from a Daytona API error response.
+ */
+export async function extractDaytonaError(response: Response, fallback: string): Promise {
+ try {
+ const data = await response.json()
+ if (typeof data?.message === 'string') return data.message
+ if (Array.isArray(data?.message)) return data.message.join(', ')
+ if (typeof data?.error === 'string') return data.error
+ } catch {
+ // Non-JSON error body; fall through to the fallback message
+ }
+ return `${fallback} (status ${response.status})`
+}
+
+/**
+ * Coerces an optional user- or LLM-provided value to a number, treating
+ * empty/missing values as undefined.
+ */
+export function toOptionalNumber(value: unknown): number | undefined {
+ if (value === undefined || value === null || value === '') return undefined
+ const num = Number(value)
+ return Number.isNaN(num) ? undefined : num
+}
+
+/**
+ * Coerces an optional user- or LLM-provided value to a boolean, treating
+ * empty/missing values as undefined.
+ */
+export function toOptionalBoolean(value: unknown): boolean | undefined {
+ if (value === undefined || value === null || value === '') return undefined
+ if (typeof value === 'boolean') return value
+ return value === 'true'
+}
+
+/**
+ * Maps a raw Daytona sandbox object to the normalized summary shape.
+ */
+export function mapDaytonaSandbox(sandbox: Record): DaytonaSandboxSummary {
+ return {
+ id: sandbox.id ?? '',
+ name: sandbox.name ?? '',
+ state: sandbox.state ?? null,
+ snapshot: sandbox.snapshot ?? null,
+ target: sandbox.target ?? null,
+ cpu: sandbox.cpu ?? null,
+ gpu: sandbox.gpu ?? null,
+ memory: sandbox.memory ?? null,
+ disk: sandbox.disk ?? null,
+ labels: sandbox.labels ?? {},
+ public: sandbox.public ?? null,
+ errorReason: sandbox.errorReason ?? null,
+ autoStopInterval: sandbox.autoStopInterval ?? null,
+ createdAt: sandbox.createdAt ?? null,
+ updatedAt: sandbox.updatedAt ?? null,
+ }
+}
+
+/**
+ * Shared output property map for sandbox summary outputs.
+ */
+export const DAYTONA_SANDBOX_OUTPUT_PROPERTIES = {
+ id: { type: 'string', description: 'Sandbox ID' },
+ name: { type: 'string', description: 'Sandbox name' },
+ state: { type: 'string', description: 'Sandbox state (e.g., started, stopped)', optional: true },
+ snapshot: {
+ type: 'string',
+ description: 'Snapshot the sandbox was created from',
+ optional: true,
+ },
+ target: { type: 'string', description: 'Region the sandbox runs in', optional: true },
+ cpu: { type: 'number', description: 'CPU cores allocated', optional: true },
+ gpu: { type: 'number', description: 'GPU units allocated', optional: true },
+ memory: { type: 'number', description: 'Memory allocated in GB', optional: true },
+ disk: { type: 'number', description: 'Disk space allocated in GB', optional: true },
+ labels: { type: 'json', description: 'Labels attached to the sandbox', optional: true },
+ public: { type: 'boolean', description: 'Whether the HTTP preview is public', optional: true },
+ errorReason: { type: 'string', description: 'Error reason if in error state', optional: true },
+ autoStopInterval: {
+ type: 'number',
+ description: 'Auto-stop interval in minutes (0 means disabled)',
+ optional: true,
+ },
+ createdAt: { type: 'string', description: 'Creation timestamp', optional: true },
+ updatedAt: { type: 'string', description: 'Last update timestamp', optional: true },
+} as const
diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts
index 7040ec8d73..f197492d44 100644
--- a/apps/sim/tools/registry.ts
+++ b/apps/sim/tools/registry.ts
@@ -611,6 +611,20 @@ import {
datadogSendLogsTool,
datadogSubmitMetricsTool,
} from '@/tools/datadog'
+import {
+ daytonaCreateSandboxTool,
+ daytonaDeleteSandboxTool,
+ daytonaDownloadFileTool,
+ daytonaExecuteCommandTool,
+ daytonaGetSandboxTool,
+ daytonaGitCloneTool,
+ daytonaListFilesTool,
+ daytonaListSandboxesTool,
+ daytonaRunCodeTool,
+ daytonaStartSandboxTool,
+ daytonaStopSandboxTool,
+ daytonaUploadFileTool,
+} from '@/tools/daytona'
import {
devinAppendSessionTagsTool,
devinArchiveSessionTool,
@@ -4577,6 +4591,18 @@ export const tools: Record = {
databricks_list_runs: databricksListRunsTool,
databricks_list_warehouses: databricksListWarehousesTool,
databricks_run_job: databricksRunJobTool,
+ daytona_create_sandbox: daytonaCreateSandboxTool,
+ daytona_delete_sandbox: daytonaDeleteSandboxTool,
+ daytona_download_file: daytonaDownloadFileTool,
+ daytona_execute_command: daytonaExecuteCommandTool,
+ daytona_get_sandbox: daytonaGetSandboxTool,
+ daytona_git_clone: daytonaGitCloneTool,
+ daytona_list_files: daytonaListFilesTool,
+ daytona_list_sandboxes: daytonaListSandboxesTool,
+ daytona_run_code: daytonaRunCodeTool,
+ daytona_start_sandbox: daytonaStartSandboxTool,
+ daytona_stop_sandbox: daytonaStopSandboxTool,
+ daytona_upload_file: daytonaUploadFileTool,
dub_bulk_create_links: dubBulkCreateLinksTool,
dub_bulk_delete_links: dubBulkDeleteLinksTool,
dub_bulk_update_links: dubBulkUpdateLinksTool,
From 9d94665d290df8b02b46f940f0c17d2a4169f9ee Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:09:53 -0700
Subject: [PATCH 2/7] fix(daytona): address review feedback and harden edge
cases
- Pre-check file size via userFile.size before downloading from storage in the upload route
- Tolerate empty response bodies in delete/start/stop sandbox tools
- Preserve explicit timeout 0 for run_code and execute_command
- Default missing exitCode to -1 so unknown state is distinguishable from success
- Reject blank sandbox IDs, clamp list limit to 1-200, trim destination path
- Clarify that toolbox operations require the sandbox ID (not name)
- Bump contract route baseline to 812 for the new daytona upload route
---
.../content/docs/en/integrations/daytona.mdx | 6 ++--
.../sim/app/api/tools/daytona/upload/route.ts | 21 ++++++++++--
apps/sim/blocks/blocks/daytona.ts | 18 +++++++---
apps/sim/tools/daytona/delete_sandbox.ts | 7 ++--
apps/sim/tools/daytona/execute_command.ts | 7 ++--
apps/sim/tools/daytona/get_sandbox.ts | 4 +--
apps/sim/tools/daytona/list_sandboxes.ts | 6 ++--
apps/sim/tools/daytona/run_code.ts | 7 ++--
apps/sim/tools/daytona/start_sandbox.ts | 7 ++--
apps/sim/tools/daytona/stop_sandbox.ts | 7 ++--
apps/sim/tools/daytona/utils.ts | 33 +++++++++++++++++--
scripts/check-api-validation-contracts.ts | 4 +--
12 files changed, 96 insertions(+), 31 deletions(-)
diff --git a/apps/docs/content/docs/en/integrations/daytona.mdx b/apps/docs/content/docs/en/integrations/daytona.mdx
index 054c420a73..eb9f323871 100644
--- a/apps/docs/content/docs/en/integrations/daytona.mdx
+++ b/apps/docs/content/docs/en/integrations/daytona.mdx
@@ -79,7 +79,7 @@ List Daytona sandboxes in the organization
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Daytona API key |
-| `limit` | number | No | Maximum number of sandboxes to return |
+| `limit` | number | No | Maximum number of sandboxes to return \(1-200\) |
| `name` | string | No | Filter sandboxes by name prefix \(case-insensitive\) |
| `labels` | json | No | Filter sandboxes by labels as key-value pairs |
| `cursor` | string | No | Pagination cursor from a previous response |
@@ -178,7 +178,7 @@ Execute a shell command inside a Daytona sandbox
| Parameter | Type | Description |
| --------- | ---- | ----------- |
-| `exitCode` | number | Exit code of the command |
+| `exitCode` | number | Exit code of the command \(-1 if missing from the response\) |
| `result` | string | Combined stdout/stderr output of the command |
### `daytona_run_code`
@@ -200,7 +200,7 @@ Run Python, JavaScript, or TypeScript code inside a Daytona sandbox
| Parameter | Type | Description |
| --------- | ---- | ----------- |
-| `exitCode` | number | Exit code of the code run |
+| `exitCode` | number | Exit code of the code run \(-1 if missing from the response\) |
| `result` | string | Combined stdout/stderr output of the code run |
| `artifacts` | json | Artifacts produced by the run \(e.g., matplotlib charts\) |
diff --git a/apps/sim/app/api/tools/daytona/upload/route.ts b/apps/sim/app/api/tools/daytona/upload/route.ts
index a5dc58651e..17b18445f6 100644
--- a/apps/sim/app/api/tools/daytona/upload/route.ts
+++ b/apps/sim/app/api/tools/daytona/upload/route.ts
@@ -51,6 +51,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const denied = await assertToolFileAccess(userFile.key, authResult.userId, requestId, logger)
if (denied) return denied
+ if (userFile.size > MAX_UPLOAD_SIZE_BYTES) {
+ const sizeMB = (userFile.size / (1024 * 1024)).toFixed(2)
+ return NextResponse.json(
+ { success: false, error: `File size (${sizeMB}MB) exceeds upload limit of 100MB` },
+ { status: 400 }
+ )
+ }
+
logger.info(`[${requestId}] Downloading file: ${userFile.name} (${userFile.size} bytes)`)
fileBuffer = await downloadFileFromStorage(userFile, requestId, logger)
fileName = params.fileName || userFile.name
@@ -70,9 +78,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
}
- const destinationPath = params.destinationPath.endsWith('/')
- ? `${params.destinationPath}${fileName}`
- : params.destinationPath
+ const requestedPath = params.destinationPath.trim()
+ if (!requestedPath) {
+ return NextResponse.json(
+ { success: false, error: 'Destination path is required' },
+ { status: 400 }
+ )
+ }
+ const destinationPath = requestedPath.endsWith('/')
+ ? `${requestedPath}${fileName}`
+ : requestedPath
logger.info(
`[${requestId}] Uploading to Daytona sandbox ${params.sandboxId}: ${destinationPath} (${fileBuffer.length} bytes)`
diff --git a/apps/sim/blocks/blocks/daytona.ts b/apps/sim/blocks/blocks/daytona.ts
index 8f5757c51d..aa936c708b 100644
--- a/apps/sim/blocks/blocks/daytona.ts
+++ b/apps/sim/blocks/blocks/daytona.ts
@@ -62,7 +62,9 @@ export const DaytonaBlock: BlockConfig = {
id: 'sandboxId',
title: 'Sandbox ID',
type: 'short-input',
- placeholder: 'ID or name of the sandbox',
+ placeholder: 'ID of the sandbox',
+ description:
+ 'Get, start, stop, and delete also accept the sandbox name; all other operations require the ID',
condition: { field: 'operation', value: SANDBOX_SCOPED_OPERATIONS },
required: { field: 'operation', value: SANDBOX_SCOPED_OPERATIONS },
},
@@ -430,13 +432,17 @@ export const DaytonaBlock: BlockConfig = {
baseParams.code = rest.code
baseParams.language = rest.language
if (rest.runEnv) baseParams.env = rest.runEnv
- if (rest.timeout) baseParams.timeout = Number(rest.timeout)
+ if (rest.timeout !== undefined && rest.timeout !== '') {
+ baseParams.timeout = Number(rest.timeout)
+ }
break
case 'execute_command':
baseParams.command = rest.command
if (rest.cwd) baseParams.cwd = rest.cwd
if (rest.runEnv) baseParams.env = rest.runEnv
- if (rest.timeout) baseParams.timeout = Number(rest.timeout)
+ if (rest.timeout !== undefined && rest.timeout !== '') {
+ baseParams.timeout = Number(rest.timeout)
+ }
break
case 'upload_file': {
const normalizedFile = normalizeFileInput(rest.file, { single: true })
@@ -475,7 +481,11 @@ export const DaytonaBlock: BlockConfig = {
inputs: {
operation: { type: 'string', description: 'Operation to perform' },
apiKey: { type: 'string', description: 'Daytona API key' },
- sandboxId: { type: 'string', description: 'ID or name of the sandbox' },
+ sandboxId: {
+ type: 'string',
+ description:
+ 'Sandbox ID (get, start, stop, and delete operations also accept the sandbox name)',
+ },
snapshot: { type: 'string', description: 'Snapshot to create the sandbox from' },
sandboxName: { type: 'string', description: 'Name for the sandbox' },
target: { type: 'string', description: 'Region for the sandbox' },
diff --git a/apps/sim/tools/daytona/delete_sandbox.ts b/apps/sim/tools/daytona/delete_sandbox.ts
index 9c622f863e..6508929835 100644
--- a/apps/sim/tools/daytona/delete_sandbox.ts
+++ b/apps/sim/tools/daytona/delete_sandbox.ts
@@ -2,8 +2,10 @@ import type { DaytonaDeleteSandboxParams, DaytonaSandboxResponse } from '@/tools
import {
DAYTONA_API_BASE_URL,
DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ encodeSandboxId,
extractDaytonaError,
mapDaytonaSandbox,
+ parseDaytonaJson,
} from '@/tools/daytona/utils'
import type { ToolConfig } from '@/tools/types'
@@ -32,8 +34,7 @@ export const daytonaDeleteSandboxTool: ToolConfig<
},
request: {
- url: (params) =>
- `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}`,
+ url: (params) => `${DAYTONA_API_BASE_URL}/sandbox/${encodeSandboxId(params.sandboxId)}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -44,7 +45,7 @@ export const daytonaDeleteSandboxTool: ToolConfig<
if (!response.ok) {
throw new Error(await extractDaytonaError(response, 'Failed to delete sandbox'))
}
- const data = await response.json()
+ const data = await parseDaytonaJson(response)
return {
success: true,
output: {
diff --git a/apps/sim/tools/daytona/execute_command.ts b/apps/sim/tools/daytona/execute_command.ts
index be35c18b1f..6165e5ea38 100644
--- a/apps/sim/tools/daytona/execute_command.ts
+++ b/apps/sim/tools/daytona/execute_command.ts
@@ -82,14 +82,17 @@ export const daytonaExecuteCommandTool: ToolConfig<
return {
success: true,
output: {
- exitCode: data.exitCode ?? 0,
+ exitCode: data.exitCode ?? -1,
result: data.result ?? '',
},
}
},
outputs: {
- exitCode: { type: 'number', description: 'Exit code of the command' },
+ exitCode: {
+ type: 'number',
+ description: 'Exit code of the command (-1 if missing from the response)',
+ },
result: { type: 'string', description: 'Combined stdout/stderr output of the command' },
},
}
diff --git a/apps/sim/tools/daytona/get_sandbox.ts b/apps/sim/tools/daytona/get_sandbox.ts
index 2b6c59efea..fddd8f71ba 100644
--- a/apps/sim/tools/daytona/get_sandbox.ts
+++ b/apps/sim/tools/daytona/get_sandbox.ts
@@ -2,6 +2,7 @@ import type { DaytonaGetSandboxParams, DaytonaSandboxResponse } from '@/tools/da
import {
DAYTONA_API_BASE_URL,
DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ encodeSandboxId,
extractDaytonaError,
mapDaytonaSandbox,
} from '@/tools/daytona/utils'
@@ -29,8 +30,7 @@ export const daytonaGetSandboxTool: ToolConfig
- `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}`,
+ url: (params) => `${DAYTONA_API_BASE_URL}/sandbox/${encodeSandboxId(params.sandboxId)}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
diff --git a/apps/sim/tools/daytona/list_sandboxes.ts b/apps/sim/tools/daytona/list_sandboxes.ts
index 044aad49da..9b7719a5ae 100644
--- a/apps/sim/tools/daytona/list_sandboxes.ts
+++ b/apps/sim/tools/daytona/list_sandboxes.ts
@@ -32,7 +32,7 @@ export const daytonaListSandboxesTool: ToolConfig<
type: 'number',
required: false,
visibility: 'user-or-llm',
- description: 'Maximum number of sandboxes to return',
+ description: 'Maximum number of sandboxes to return (1-200)',
},
name: {
type: 'string',
@@ -58,7 +58,9 @@ export const daytonaListSandboxesTool: ToolConfig<
url: (params) => {
const query = new URLSearchParams()
const limit = toOptionalNumber(params.limit)
- if (limit !== undefined) query.set('limit', String(limit))
+ if (limit !== undefined) {
+ query.set('limit', String(Math.min(Math.max(Math.trunc(limit), 1), 200)))
+ }
if (params.name) query.set('name', params.name)
const labels = transformTable(params.labels ?? null)
if (Object.keys(labels).length > 0) query.set('labels', JSON.stringify(labels))
diff --git a/apps/sim/tools/daytona/run_code.ts b/apps/sim/tools/daytona/run_code.ts
index 317658307c..a9e8dc5ce4 100644
--- a/apps/sim/tools/daytona/run_code.ts
+++ b/apps/sim/tools/daytona/run_code.ts
@@ -76,7 +76,7 @@ export const daytonaRunCodeTool: ToolConfig
- `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}/start`,
+ url: (params) => `${DAYTONA_API_BASE_URL}/sandbox/${encodeSandboxId(params.sandboxId)}/start`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -44,7 +45,7 @@ export const daytonaStartSandboxTool: ToolConfig<
if (!response.ok) {
throw new Error(await extractDaytonaError(response, 'Failed to start sandbox'))
}
- const data = await response.json()
+ const data = await parseDaytonaJson(response)
return {
success: true,
output: {
diff --git a/apps/sim/tools/daytona/stop_sandbox.ts b/apps/sim/tools/daytona/stop_sandbox.ts
index 15a2a22410..c9eb2b0026 100644
--- a/apps/sim/tools/daytona/stop_sandbox.ts
+++ b/apps/sim/tools/daytona/stop_sandbox.ts
@@ -2,8 +2,10 @@ import type { DaytonaSandboxResponse, DaytonaStopSandboxParams } from '@/tools/d
import {
DAYTONA_API_BASE_URL,
DAYTONA_SANDBOX_OUTPUT_PROPERTIES,
+ encodeSandboxId,
extractDaytonaError,
mapDaytonaSandbox,
+ parseDaytonaJson,
} from '@/tools/daytona/utils'
import type { ToolConfig } from '@/tools/types'
@@ -30,8 +32,7 @@ export const daytonaStopSandboxTool: ToolConfig
- `${DAYTONA_API_BASE_URL}/sandbox/${encodeURIComponent(params.sandboxId.trim())}/stop`,
+ url: (params) => `${DAYTONA_API_BASE_URL}/sandbox/${encodeSandboxId(params.sandboxId)}/stop`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey}`,
@@ -42,7 +43,7 @@ export const daytonaStopSandboxTool: ToolConfig> {
+ const text = await response.text()
+ if (!text) return {}
+ try {
+ return JSON.parse(text)
+ } catch {
+ return {}
+ }
}
/**
@@ -43,7 +69,10 @@ export function toOptionalNumber(value: unknown): number | undefined {
export function toOptionalBoolean(value: unknown): boolean | undefined {
if (value === undefined || value === null || value === '') return undefined
if (typeof value === 'boolean') return value
- return value === 'true'
+ const normalized = String(value).trim().toLowerCase()
+ if (normalized === 'true') return true
+ if (normalized === 'false') return false
+ return undefined
}
/**
diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts
index 90bad6c5a4..645b41e592 100644
--- a/scripts/check-api-validation-contracts.ts
+++ b/scripts/check-api-validation-contracts.ts
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
- totalRoutes: 811,
- zodRoutes: 811,
+ totalRoutes: 812,
+ zodRoutes: 812,
nonZodRoutes: 0,
} as const
From 9967eccbb67922e17f8652b096443bdf9f26fe10 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:17:43 -0700
Subject: [PATCH 3/7] fix(daytona): cap download size at 100MB and preserve
sandbox identity on empty lifecycle responses
---
apps/sim/tools/daytona/delete_sandbox.ts | 8 ++++++--
apps/sim/tools/daytona/download_file.ts | 15 +++++++++++++++
apps/sim/tools/daytona/start_sandbox.ts | 8 ++++++--
apps/sim/tools/daytona/stop_sandbox.ts | 8 ++++++--
4 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/apps/sim/tools/daytona/delete_sandbox.ts b/apps/sim/tools/daytona/delete_sandbox.ts
index 6508929835..830092dcb8 100644
--- a/apps/sim/tools/daytona/delete_sandbox.ts
+++ b/apps/sim/tools/daytona/delete_sandbox.ts
@@ -41,15 +41,19 @@ export const daytonaDeleteSandboxTool: ToolConfig<
}),
},
- transformResponse: async (response) => {
+ transformResponse: async (response, params) => {
if (!response.ok) {
throw new Error(await extractDaytonaError(response, 'Failed to delete sandbox'))
}
const data = await parseDaytonaJson(response)
+ const sandbox = mapDaytonaSandbox(data)
+ if (!sandbox.id && params) {
+ sandbox.id = params.sandboxId.trim()
+ }
return {
success: true,
output: {
- sandbox: mapDaytonaSandbox(data),
+ sandbox,
},
}
},
diff --git a/apps/sim/tools/daytona/download_file.ts b/apps/sim/tools/daytona/download_file.ts
index efccd8331c..201bd6fa5a 100644
--- a/apps/sim/tools/daytona/download_file.ts
+++ b/apps/sim/tools/daytona/download_file.ts
@@ -2,6 +2,13 @@ import type { DaytonaDownloadFileParams, DaytonaDownloadFileResponse } from '@/t
import { daytonaToolboxUrl, extractDaytonaError } from '@/tools/daytona/utils'
import type { ToolConfig } from '@/tools/types'
+const MAX_DOWNLOAD_SIZE_BYTES = 100 * 1024 * 1024
+
+function downloadSizeError(bytes: number): Error {
+ const sizeMB = (bytes / (1024 * 1024)).toFixed(2)
+ return new Error(`File size (${sizeMB}MB) exceeds download limit of 100MB`)
+}
+
export const daytonaDownloadFileTool: ToolConfig<
DaytonaDownloadFileParams,
DaytonaDownloadFileResponse
@@ -49,10 +56,18 @@ export const daytonaDownloadFileTool: ToolConfig<
throw new Error(await extractDaytonaError(response, 'Failed to download file'))
}
+ const contentLength = Number(response.headers.get('content-length'))
+ if (Number.isFinite(contentLength) && contentLength > MAX_DOWNLOAD_SIZE_BYTES) {
+ throw downloadSizeError(contentLength)
+ }
+
const mimeType = response.headers.get('content-type') || 'application/octet-stream'
const fileName = params?.filePath.trim().split('/').filter(Boolean).pop() || 'download'
const arrayBuffer = await response.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
+ if (buffer.length > MAX_DOWNLOAD_SIZE_BYTES) {
+ throw downloadSizeError(buffer.length)
+ }
return {
success: true,
diff --git a/apps/sim/tools/daytona/start_sandbox.ts b/apps/sim/tools/daytona/start_sandbox.ts
index e77b0bd90a..5c1b5ce920 100644
--- a/apps/sim/tools/daytona/start_sandbox.ts
+++ b/apps/sim/tools/daytona/start_sandbox.ts
@@ -41,15 +41,19 @@ export const daytonaStartSandboxTool: ToolConfig<
}),
},
- transformResponse: async (response) => {
+ transformResponse: async (response, params) => {
if (!response.ok) {
throw new Error(await extractDaytonaError(response, 'Failed to start sandbox'))
}
const data = await parseDaytonaJson(response)
+ const sandbox = mapDaytonaSandbox(data)
+ if (!sandbox.id && params) {
+ sandbox.id = params.sandboxId.trim()
+ }
return {
success: true,
output: {
- sandbox: mapDaytonaSandbox(data),
+ sandbox,
},
}
},
diff --git a/apps/sim/tools/daytona/stop_sandbox.ts b/apps/sim/tools/daytona/stop_sandbox.ts
index c9eb2b0026..e0f350db3c 100644
--- a/apps/sim/tools/daytona/stop_sandbox.ts
+++ b/apps/sim/tools/daytona/stop_sandbox.ts
@@ -39,15 +39,19 @@ export const daytonaStopSandboxTool: ToolConfig {
+ transformResponse: async (response, params) => {
if (!response.ok) {
throw new Error(await extractDaytonaError(response, 'Failed to stop sandbox'))
}
const data = await parseDaytonaJson(response)
+ const sandbox = mapDaytonaSandbox(data)
+ if (!sandbox.id && params) {
+ sandbox.id = params.sandboxId.trim()
+ }
return {
success: true,
output: {
- sandbox: mapDaytonaSandbox(data),
+ sandbox,
},
}
},
From 57940d94eaf0d7196523a6599742378d9872473d Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:17:44 -0700
Subject: [PATCH 4/7] improvement(notion): black icon on white background to
match brand
---
apps/docs/components/icons.tsx | 2 +-
apps/docs/content/docs/en/integrations/notion.mdx | 2 +-
apps/sim/blocks/blocks/notion.ts | 4 ++--
apps/sim/components/icons.tsx | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx
index 23d411b218..1368eb93b3 100644
--- a/apps/docs/components/icons.tsx
+++ b/apps/docs/components/icons.tsx
@@ -1145,7 +1145,7 @@ export function NotionIcon(props: SVGProps) {
)
diff --git a/apps/docs/content/docs/en/integrations/notion.mdx b/apps/docs/content/docs/en/integrations/notion.mdx
index 4f0edfa5bd..4ddf64db96 100644
--- a/apps/docs/content/docs/en/integrations/notion.mdx
+++ b/apps/docs/content/docs/en/integrations/notion.mdx
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
{/* MANUAL-CONTENT-START:intro */}
diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts
index fca0932cfb..01fae3421e 100644
--- a/apps/sim/blocks/blocks/notion.ts
+++ b/apps/sim/blocks/blocks/notion.ts
@@ -19,7 +19,7 @@ export const NotionBlock: BlockConfig = {
docsLink: 'https://docs.sim.ai/integrations/notion',
category: 'tools',
integrationType: IntegrationType.Documents,
- bgColor: '#181C1E',
+ bgColor: '#FFFFFF',
icon: NotionIcon,
subBlocks: [
{
@@ -430,7 +430,7 @@ export const NotionV2Block: BlockConfig = {
docsLink: 'https://docs.sim.ai/integrations/notion',
category: 'tools',
integrationType: IntegrationType.Documents,
- bgColor: '#181C1E',
+ bgColor: '#FFFFFF',
icon: NotionIcon,
hideFromToolbar: false,
subBlocks: [
diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx
index 23d411b218..1368eb93b3 100644
--- a/apps/sim/components/icons.tsx
+++ b/apps/sim/components/icons.tsx
@@ -1145,7 +1145,7 @@ export function NotionIcon(props: SVGProps) {
)
From 18c0273849c19e8f852408f01d5e9fad54ae6881 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:21:27 -0700
Subject: [PATCH 5/7] fix(daytona): reject oversized base64 uploads before
decoding
---
apps/sim/app/api/tools/daytona/upload/route.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/apps/sim/app/api/tools/daytona/upload/route.ts b/apps/sim/app/api/tools/daytona/upload/route.ts
index 17b18445f6..52fb2ec70a 100644
--- a/apps/sim/app/api/tools/daytona/upload/route.ts
+++ b/apps/sim/app/api/tools/daytona/upload/route.ts
@@ -64,6 +64,14 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
fileName = params.fileName || userFile.name
} else if (params.fileContent) {
logger.info(`[${requestId}] Using legacy base64 content input`)
+ const estimatedSize = Math.floor((params.fileContent.length * 3) / 4)
+ if (estimatedSize > MAX_UPLOAD_SIZE_BYTES) {
+ const sizeMB = (estimatedSize / (1024 * 1024)).toFixed(2)
+ return NextResponse.json(
+ { success: false, error: `File size (${sizeMB}MB) exceeds upload limit of 100MB` },
+ { status: 400 }
+ )
+ }
fileBuffer = Buffer.from(params.fileContent, 'base64')
fileName = params.fileName || 'file'
} else {
From cee3094ca9e959f4bce4a4bf82f395a0084dd0af Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:45:20 -0700
Subject: [PATCH 6/7] improvement(landing): move integration last-updated below
CTAs and de-emphasize it
---
.../integrations/(shell)/[slug]/page.tsx | 8 +--
apps/sim/lib/integrations/integrations.json | 69 ++++++++++++++++++-
2 files changed, 72 insertions(+), 5 deletions(-)
diff --git a/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx
index 35c7630ed5..0b7c616dd8 100644
--- a/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx
+++ b/apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx
@@ -460,10 +460,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
Free to start at sim.ai.
-
- Last updated
-
-
{/* CTAs */}
+
+
+ Last updated
+
{/* Full-width divider */}
diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json
index 84f5f663b6..b12e346a77 100644
--- a/apps/sim/lib/integrations/integrations.json
+++ b/apps/sim/lib/integrations/integrations.json
@@ -3703,6 +3703,73 @@
"integrationType": "observability",
"tags": ["monitoring", "incident-management", "error-tracking"]
},
+ {
+ "type": "daytona",
+ "slug": "daytona",
+ "name": "Daytona",
+ "description": "Run code and commands in secure cloud sandboxes",
+ "longDescription": "Integrate Daytona into your workflow to run AI-generated code in secure, isolated sandboxes. Create and manage sandboxes, execute shell commands, run Python, JavaScript, or TypeScript code, transfer files, and clone Git repositories.",
+ "bgColor": "#FFFFFF",
+ "iconName": "DaytonaIcon",
+ "docsUrl": "https://docs.sim.ai/integrations/daytona",
+ "operations": [
+ {
+ "name": "Create Sandbox",
+ "description": "Create a new Daytona sandbox for running AI-generated code in isolation"
+ },
+ {
+ "name": "Run Code",
+ "description": "Run Python, JavaScript, or TypeScript code inside a Daytona sandbox"
+ },
+ {
+ "name": "Execute Command",
+ "description": "Execute a shell command inside a Daytona sandbox"
+ },
+ {
+ "name": "Upload File",
+ "description": "Upload a file to a Daytona sandbox"
+ },
+ {
+ "name": "Download File",
+ "description": "Download a file from a Daytona sandbox"
+ },
+ {
+ "name": "List Files",
+ "description": "List files in a directory of a Daytona sandbox"
+ },
+ {
+ "name": "Git Clone",
+ "description": "Clone a Git repository into a Daytona sandbox"
+ },
+ {
+ "name": "List Sandboxes",
+ "description": "List Daytona sandboxes in the organization"
+ },
+ {
+ "name": "Get Sandbox",
+ "description": "Get details of a Daytona sandbox"
+ },
+ {
+ "name": "Start Sandbox",
+ "description": "Start a stopped Daytona sandbox"
+ },
+ {
+ "name": "Stop Sandbox",
+ "description": "Stop a running Daytona sandbox"
+ },
+ {
+ "name": "Delete Sandbox",
+ "description": "Delete a Daytona sandbox"
+ }
+ ],
+ "operationCount": 12,
+ "triggers": [],
+ "triggerCount": 0,
+ "authType": "api-key",
+ "category": "tools",
+ "integrationType": "devops",
+ "tags": ["agentic", "cloud", "automation"]
+ },
{
"type": "devin",
"slug": "devin",
@@ -10348,7 +10415,7 @@
"name": "Notion",
"description": "Manage Notion pages",
"longDescription": "Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace.",
- "bgColor": "#181C1E",
+ "bgColor": "#FFFFFF",
"iconName": "NotionIcon",
"docsUrl": "https://docs.sim.ai/integrations/notion",
"operations": [],
From abd1849868e741a89f922ccd7acad19313f1e5d7 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 11 Jun 2026 18:55:53 -0700
Subject: [PATCH 7/7] fix(daytona): forward explicit zero cpu/memory/disk
values in create sandbox
---
apps/sim/blocks/blocks/daytona.ts | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/apps/sim/blocks/blocks/daytona.ts b/apps/sim/blocks/blocks/daytona.ts
index aa936c708b..799f127c55 100644
--- a/apps/sim/blocks/blocks/daytona.ts
+++ b/apps/sim/blocks/blocks/daytona.ts
@@ -414,9 +414,11 @@ export const DaytonaBlock: BlockConfig = {
if (rest.sandboxUser) baseParams.user = rest.sandboxUser
if (rest.envVars) baseParams.env = rest.envVars
if (rest.sandboxLabels) baseParams.labels = rest.sandboxLabels
- if (rest.cpu) baseParams.cpu = Number(rest.cpu)
- if (rest.memory) baseParams.memory = Number(rest.memory)
- if (rest.disk) baseParams.disk = Number(rest.disk)
+ if (rest.cpu !== undefined && rest.cpu !== '') baseParams.cpu = Number(rest.cpu)
+ if (rest.memory !== undefined && rest.memory !== '') {
+ baseParams.memory = Number(rest.memory)
+ }
+ if (rest.disk !== undefined && rest.disk !== '') baseParams.disk = Number(rest.disk)
if (rest.autoStopInterval !== undefined && rest.autoStopInterval !== '') {
baseParams.autoStopInterval = Number(rest.autoStopInterval)
}