# Blaxel TypeScript SDK [Blaxel](https://blaxel.ai) is a perpetual sandbox platform that achieves near instant latency by keeping infinite secure sandboxes on automatic standby, while co-hosting your agent logic to cut network overhead. This repository contains Blaxel's TypeScript SDK, which lets you create and manage sandboxes and other resources on Blaxel. ## Installation ```bash # npm npm install @blaxel/core # yarn yarn add @blaxel/core # bun bun add @blaxel/core ``` ## Authentication The SDK authenticates with your Blaxel workspace using these sources (in priority order): 1. Blaxel CLI, when logged in 2. Environment variables in `.env` file (`BL_WORKSPACE`, `BL_API_KEY`) 3. System environment variables 4. Blaxel configuration file (`~/.blaxel/config.yaml`) When developing locally, the recommended method is to just log in to your workspace with the Blaxel CLI: ```bash bl login YOUR-WORKSPACE ``` This allows you to run Blaxel SDK functions that will automatically connect to your workspace without additional setup. When you deploy on Blaxel, this connection persists automatically. When running Blaxel SDK from a remote server that is not Blaxel-hosted, we recommend using environment variables as described in the third option above. ## API error types `@blaxel/core` exports `BlaxelErrorCode` (gateway, auth and sandbox-creation code unions), `BlaxelErrorCodeValue` (also unknown string codes and numeric HTTP codes), and body types: `BlaxelApiErrorBody`, `BlaxelActionErrorBody`, `BlaxelPlatformErrorBody`, `BlaxelSandboxApiErrorBody`. Errors are **not wrapped or changed**. The SDK still throws raw control-plane bodies, existing sandbox error classes, plain `Error`, or text/HTML bodies. `isBlaxelError` is true only for the shapes the SDK throws for API errors (`BlaxelErrorLike`): `ResponseError` (including `SandboxGatewayError`), `SandboxCreationTimeoutError`, and raw control-plane error bodies that carry a code and status. It is false for plain `Error`s (a network error with a code such as `ECONNRESET` included), strings, raw HTML/text bodies and empty bodies. Like `is_blaxel_error` in the Python SDK, it is false for plain exceptions and strings. Unlike Python, a control-plane error with a non-JSON body (an HTML 502, say) is thrown as a bare string here, so the guard is false for it; Python raises `UnexpectedStatus`, for which `is_blaxel_error` is true. Use the non-throwing accessors instead of assuming common properties: ```typescript import { SandboxInstance, isBlaxelError, getBlaxelErrorCode, getBlaxelErrorStatus, getBlaxelErrorMessage, getBlaxelErrorRequestId, } from "@blaxel/core"; try { await SandboxInstance.create({ name: "my-sandbox", region: "us-was-1" }); } catch (err) { if (isBlaxelError(err)) { console.error(getBlaxelErrorCode(err), getBlaxelErrorStatus(err), getBlaxelErrorMessage(err), getBlaxelErrorRequestId(err)); } throw err; } ``` Unknown codes need a fallback. Missing fields return `undefined`; in particular, raw control-plane bodies and creation-timeout errors do not retain response headers/request IDs, and `getBlaxelErrorStatus` is `undefined` for transport errors such as an HTTP/2 stream reset. All accessors accept `unknown` safely, including values the guard rejects. `POLICY_VIOLATION` is reserved, not emitted today. ## Usage ### Sandboxes Sandboxes are secure, instant-launching compute environments that scale to zero after inactivity and resume in under 25ms. > **Base image contents:** The default `blaxel/base-image:latest` is Alpine Linux with **Node.js 22** and **git** pre-installed. It does **not** include Python or other language runtimes. To use Python, either specify `blaxel/py-app:latest` as your image (Python 3.12) or install it in the base image with `apk add --no-cache python3 py3-pip`. ```typescript import { SandboxInstance } from "@blaxel/core"; // Create a new sandbox const sandbox = await SandboxInstance.createIfNotExists({ name: "my-sandbox", image: "blaxel/base-image:latest", memory: 4096, region: "us-pdx-1", ports: [{ target: 3000, protocol: "HTTP" }], labels: { env: "dev", project: "my-project" }, ttl: "24h" }); // Get existing sandbox const existing = await SandboxInstance.get("my-sandbox"); // Delete sandbox (using class) await SandboxInstance.delete("my-sandbox"); // Delete sandbox (using instance) await existing.delete(); ``` #### Listing resources List methods return one page at a time. The result contains `data`, `meta`, and helpers for fetching more pages when you want them. ```typescript import { SandboxInstance } from "@blaxel/core"; const page = await SandboxInstance.list({ limit: 50, sort: "createdAt:desc", }); for (const sandbox of page.data) { console.log(sandbox.metadata.name); } if (page.hasMore) { const nextPage = await page.nextPage(); console.log(nextPage?.data); } // Optional auto-pagination, inspired by Stripe's SDK. for await (const sandbox of page) { console.log(sandbox.metadata.name); } const firstThousand = await page.autoPagingToArray({ limit: 1000 }); ``` #### Preview URLs Generate public preview URLs to access services running in your sandbox: ```typescript import { SandboxInstance } from "@blaxel/core"; // Get existing sandbox const sandbox = await SandboxInstance.get("my-sandbox"); // Start a web server in the sandbox await sandbox.process.exec({ command: "npm run dev -- --port 3000", workingDir: "/app", waitForPorts: [3000] }); // Create a public preview URL const preview = await sandbox.previews.createIfNotExists({ metadata: { name: "app-preview" }, spec: { port: 3000, public: true } }); console.log(preview.spec?.url); // https://xyz.preview.bl.run ``` Previews can also be private, with or without a custom prefix. When you create a private preview URL, a [token](https://docs.blaxel.ai/Sandboxes/Preview-url#private-preview-urls) is required to access the URL, passed as a request parameter or request header. ```typescript // ... // Shorthand: preview-3000, private on creation, no prefix required. const privatePreview = await sandbox.previews.createIfNotExists({ port: 3000 }); const token = await privatePreview.tokens.createIfExpired(); const url = new URL(privatePreview.url); // base URL, without a token url.searchParams.set("bl_preview_token", token.value); // credential: do not log // When no longer needed: await privatePreview.tokens.delete(token.name); // Create a public preview URL with a custom prefix const customPreview = await sandbox.previews.createIfNotExists({ metadata: { name: "custom-app-preview" }, spec: { port: 3000, prefixUrl: "my-app", public: true } }); ``` The shorthand name is constant per port within each sandbox. You can supply `name` and `public`; custom prefixes and other advanced settings still use the full model above. Existing previews are returned **as-is**, even if their port or public setting differs: the private default only applies on creation. `createIfExpired(expiresAt?, minValidity?)` defaults to a new 24-hour expiry and one hour of minimum remaining validity (milliseconds). It reuses the latest valid, nonempty token no later than the requested expiry, so an explicitly shorter expiry never reuses a longer-lived credential. Token names are generated and exposed for deletion. Concurrent callers can mint separate tokens; old tokens are not deleted. The helpers simplify setup, not its network request count or atomicity. #### Process execution Execute and manage processes in your sandbox: ```typescript import { SandboxInstance } from "@blaxel/core"; // Get existing sandbox const sandbox = await SandboxInstance.get("my-sandbox"); // Execute a command const process = await sandbox.process.exec({ name: "build-process", command: "npm run build", workingDir: "/app", waitForCompletion: true, timeout: 60 // 60 seconds }); // Kill a running process await sandbox.process.kill("build-process"); ``` `wait()` returns only a terminal API state. Temporary connection failures are retried until `maxWait`; use `maxWait: -1` to wait without a deadline, still cancellable with an `AbortSignal`. Other errors are propagated. A timeout or cancellation stops waiting, not the command. `streamLogs().wait()` also rejects when the stream fails. Give the command a unique name before starting it so you can reconnect after a lost response without starting it twice: ```typescript const name = crypto.randomUUID(); await sandbox.process.exec({ name, command: "npm run build" }); const result = await sandbox.process.wait(name, { maxWait: 30_000 }); // After a connection error, use get(name), wait(name), or logs(name) to reconnect. // To request termination explicitly: await sandbox.process.kill(name); // or stop(name) for SIGTERM await sandbox.process.wait(name); // If the stop/kill response is lost, inspect get(name) or wait(name) before retrying. ``` Terminal states reflect the sandbox API's report; older servers can report stopped before the OS process exits. The SDK never automatically resends process creation. Restart a process if it fails, up to a maximum number of restart attempts: ```typescript // ... // Run with auto-restart on failure process = await sandbox.process.exec({ name: "web-server", command: "npm run dev -- --host 0.0.0.0 --port 3000", restartOnFailure: true, maxRestarts: 5 }); ``` #### Filesystem operations Manage files and directories within your sandbox: ```typescript import { SandboxInstance } from "@blaxel/core"; import * as fs from "fs"; // Get existing sandbox const sandbox = await SandboxInstance.get("my-sandbox"); // Write and read text files await sandbox.fs.write("/app/config.json", '{"key": "value"}'); const content = await sandbox.fs.read("/app/config.json"); // Write and read binary files const binaryData = fs.readFileSync("./image.png"); await sandbox.fs.writeBinary("/app/image.png", binaryData); const blob = await sandbox.fs.readBinary("/app/image.png"); // Create directories await sandbox.fs.mkdir("/app/uploads"); // List files const { subdirectories, files } = await sandbox.fs.ls("/app"); // Search for text within files const matches = await sandbox.fs.grep("pattern", "/app", { caseSensitive: true, contextLines: 2, maxResults: 5, filePattern: "*.ts", excludeDirs: ["node_modules"] }); // Find files and directories matching specified patterns: const results = await sandbox.fs.find("/app", { type: "file", patterns: ["*.md", "*.html"], maxResults: 1000 }); // Watch for file changes const handle = sandbox.fs.watch("/app", (event) => { console.log(event.op, event.path); }, { withContent: true, ignore: ["node_modules", ".git"] }); // Close watcher handle.close(); ``` #### Drives Pass `mountDrives` to mount drives on the sandbox. Drives are looked up or created while the sandbox is being created (up to 5 at a time), and each is mounted once both it and the sandbox are ready. Give `driveName` for a drive that already exists, or `create` for a new one (a named drive is reused if it already exists). New drives are created in the region the sandbox request sends (`region`, or `BL_REGION`); without one, they are created once the sandbox exists, in its region. An existing drive must be in the sandbox's region too. ```typescript import { SandboxInstance } from "@blaxel/core"; const config = { image: "blaxel/base-image:latest", region: "us-was-1" }; const sandbox = await SandboxInstance.create(config, { mountDrives: [{ create: { name: "app-data" }, mountPath: "/mnt/data" }], }); await sandbox.fs.write("/mnt/data/hello.txt", "hello"); // A second sandbox can mount the same drive by name. const reader = await SandboxInstance.create(config, { mountDrives: [{ driveName: "app-data", mountPath: "/mnt/data", readOnly: true }], }); console.log(await reader.fs.read("/mnt/data/hello.txt")); // hello ``` Each entry also takes `drivePath` (a sub-folder of the drive). With `createIfNotExists`, a new drive needs a `name`. If anything fails, the drives this call created under a generated name (no `name` given) and did not mount are deleted; a drive created under a name you chose is never deleted, since a concurrent call may be using it, and is listed in `createdDrives` instead. If a drive can't be set up or mounted, `create` throws `SandboxDriveSetupError` with the `sandbox`, the `driveNames` left in place, the `createdDrives` among them that this call created (or may have created, if a response was lost) and the original `cause`; the sandbox and the mounts made so far are kept. If the sandbox itself can't be created, its error is thrown, wrapped in a `SandboxDriveSetupError` without a `sandbox` only if a drive created for it is left in place. #### Volumes Persist data by attaching and using volumes: ```typescript import { VolumeInstance, SandboxInstance } from "@blaxel/core"; // Create a volume const volume = await VolumeInstance.createIfNotExists({ name: "my-volume", size: 1024, // MB region: "us-pdx-1", labels: { env: "test", project: "12345" } }); // Attach volume to sandbox const sandbox = await SandboxInstance.createIfNotExists({ name: "my-sandbox", image: "blaxel/base-image:latest", volumes: [ { name: "my-volume", mountPath: "/data", readOnly: false } ] }); // List first page of volumes const volumes = await VolumeInstance.list(); for (const volume of volumes.data) { console.log(volume.name); } // Delete volume (using class) await VolumeInstance.delete("my-volume"); // Delete volume (using instance) await volume.delete(); ``` ### Batch jobs Blaxel lets you support agentic workflows by offloading asynchronous batch processing tasks to its scalable infrastructure, where they can run in parallel. Jobs can run multiple times within a single execution and accept optional input parameters. ```typescript import { blJob } from "@blaxel/core"; // Create and run a job execution const job = blJob("job-name"); const executionId = await job.createExecution({ tasks: [ { name: "John" }, { name: "Jane" }, { name: "Bob" } ] }); // Get execution status // Returns: "pending" | "running" | "completed" | "failed" const status = await job.getExecutionStatus(executionId); // Get execution details const execution = await job.getExecution(executionId); console.log(execution.status, execution.metadata); // Wait for completion try { const result = await job.waitForExecution(executionId, { maxWait: 300000, // 5 minutes (milliseconds) interval: 2000, // Poll every 2 seconds (milliseconds) }); console.log(`Completed: ${result.status}`); } catch (error) { console.log(`Timeout: ${error.message}`); } // List first page of executions const executions = await job.listExecutions(); for (const execution of executions.data) { console.log(execution.status); } // Delete an execution await job.deleteExecution(executionId); ``` ### Framework integrations Blaxel provides additional packages for framework-specific integrations and telemetry: - [`@blaxel/telemetry`](./@blaxel/telemetry/README.md) - OpenTelemetry instrumentation - [`@blaxel/eve-sandbox`](./@blaxel/eve-sandbox/README.md) - Durable Blaxel sandbox backend for eve - [`@blaxel/vercel`](./@blaxel/vercel/README.md) - Vercel AI SDK integration - [`@blaxel/llamaindex`](./@blaxel/llamaindex/README.md) - LlamaIndex integration - [`@blaxel/langgraph`](./@blaxel/langgraph/README.md) - LangGraph integration - [`@blaxel/mastra`](./@blaxel/mastra/README.md) - Mastra integration #### Model use Blaxel acts as a unified gateway for model APIs, centralizing access credentials, tracing and telemetry. You can integrate with any model API provider, or deploy your own custom model. When a model is deployed on Blaxel, a global API endpoint is also created to call it. The SDK includes a helper function that creates a reference to a model deployed on Blaxel and returns a framework-specific model client that routes API calls through Blaxel's unified gateway. ```typescript import { blModel } from "@blaxel/core"; // With Vercel AI SDK import { blModel } from "@blaxel/vercel"; const model = await blModel("gpt-5-mini"); // With LangChain import { blModel } from "@blaxel/langgraph"; const model = await blModel("gpt-5-mini"); // With LlamaIndex import { blModel } from "@blaxel/llamaindex"; const model = await blModel("gpt-5-mini"); // With Mastra import { blModel } from "@blaxel/mastra"; const model = await blModel("gpt-5-mini"); ``` #### MCP tool use Blaxel lets you deploy and host Model Context Protocol (MCP) servers, accessible at a global endpoint over streamable HTTP. The SDK includes a helper function that retrieves and returns tool definitions from a Blaxel-hosted MCP server in the format required by specific frameworks. ```typescript // With Vercel AI SDK import { blTools } from "@blaxel/vercel"; const tools = await blTools(['sandbox/my-sandbox']) // With Mastra import { blTools } from "@blaxel/mastra"; const tools = await blTools(['sandbox/my-sandbox']) // With LlamaIndex import { blTools } from "@blaxel/llamaindex"; const tools = await blTools(['sandbox/my-sandbox']) // With LangChain import { blTools } from "@blaxel/langgraph"; const tools = await blTools(['sandbox/my-sandbox']) ``` Here is an example of retrieving tool definitions from a Blaxel sandbox's MCP server in the format required by Vercel's AI SDK: ```typescript import { SandboxInstance } from "@blaxel/core"; import { blTools } from "@blaxel/vercel"; // Create a new sandbox const sandbox = await SandboxInstance.createIfNotExists({ name: "my-sandbox", image: "blaxel/base-image:latest", memory: 4096, region: "us-pdx-1", ttl: "24h" }); // Get sandbox tools const tools = await blTools(['sandbox/my-sandbox']) ``` ### Telemetry Instrumentation happens automatically when workloads run on Blaxel. Enable automatic telemetry by importing the `@blaxel/telemetry` package: ```typescript import "@blaxel/telemetry"; ``` ## Data collection ### Error tracking The SDK includes opt-in error tracking for unhandled exceptions whose first non-runtime frame belongs to the installed `@blaxel/core` package. Events contain an allowlisted error type, a fixed message, SDK-owned relative stack frames, the SDK version and commit, and the `dev` or `prod` environment. Raw error messages, response bodies, workspace and resource identifiers, absolute paths, and application stack frames are not collected. Caught errors, including errors logged with `console.error`, are not reported. If a browser bundler removes the package boundary from stack frames, error tracking stays disabled rather than attributing application errors to the SDK. Error tracking is off by default since v0.2.76. To explicitly disable it in older versions: ```bash export DO_NOT_TRACK=1 ``` Or add to `~/.blaxel/config.yaml`: ```yaml tracking: false ``` Where both settings exist, the `DO_NOT_TRACK` variable takes precedence. ### Telemetry Telemetry, delivered via OpenTelemetry, is controlled by the `BL_ENABLE_OPENTELEMETRY` environment variable. When you deploy an agent to Blaxel, the platform automatically injects `BL_ENABLE_OPENTELEMETRY=true` into the environment. When developing locally, this environment variable is not set and therefore defaults to `false`. To explicitly disable telemetry, override the variable in your Blaxel deployment: ```bash export BL_ENABLE_OPENTELEMETRY=false ``` For more information, refer to [our documentation](https://docs.blaxel.ai/Security/Data-collection-and-privacy). ## Requirements - Node.js v18 or later ## Contributing Contributions are welcome! Please feel free to [submit a pull request](https://github.com/blaxel-ai/sdk-typescript/pulls). ## License This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. ### Image repository and tag pagination The SDK defaults to API version `2026-09-22`. `listImages()` now returns `{ data, meta }` with repository summaries (`metadata` and `spec.size` / `spec.tagCount`), rather than an array containing every tag. Fetch tags separately with `listImageTags()` or use the lazy helpers: ```typescript import { ImageInstance } from "@blaxel/core"; const images = await ImageInstance.list({ limit: 20, sort: "name:asc", q: "my-" }); for await (const image of images) { console.log(image.metadata.name, image.spec.tagCount); } const tags = await ImageInstance.listTags("sandbox", "my-image", { limit: 20, sort: "name:desc", // sourceWorkspace: "owner-workspace", // For an account-shared image }); const first100Tags = await tags.autoPagingToArray({ limit: 100 }); ``` Each helper fetches one page initially. `nextPage()`, async iteration and `autoPagingToArray()` fetch subsequent pages on demand. Tag search supports `q` (prefix) or `name` (exact), never both; tag sorting is by name only. `BL_API_VERSION` remains available for older contracts through low-level API calls; image pagination and the typed summaries described above require `2026-09-22` or later.