TypeScript SDK
Use the Dedalus TypeScript SDK to create and manage persistent Linux machines from your application. Create one or many machines for a project, run commands, and reuse their files across sessions.
Your application calls the SDK. Commands submitted through the SDK run inside the machine. You can also change machine resources, control sleep and wake, request SSH access, and inspect usage.
Install
Use TypeScript 4.9 or later and a supported runtime. The examples below use Node.js and top-level await in an ES module.
npm install dedalus
pnpm add dedalus
yarn add dedalus
bun add dedalus
Authenticate
Create an API key in the Dedalus Dashboard, then set it in the environment:
export DEDALUS_API_KEY="your-api-key"
new Dedalus() reads DEDALUS_API_KEY automatically.
Create a machine
This creates a machine with 1 vCPU, 4,096 MiB of memory, 10 GiB of storage, and autosleep after five idle minutes:
import Dedalus from 'dedalus';
const client = new Dedalus();
const machine = await client.machines.create({
vcpu: 1,
memory_mib: 4096,
storage_gib: 10,
autosleep: '5m',
});
console.log(machine.machine_id);
Use the saved machine.machine_id in later requests for this machine.
Machine creation is asynchronous. The response contains the requested state and the machine's current lifecycle status. You can submit an execution immediately after creation. Dedalus waits for the machine to start before running the command.
Run a command
An execution is one command run inside a machine. Creating an execution returns an execution_id before the command finishes. Use that ID to check the command's status and retrieve its output.
This example writes a file, reads it back, and waits for the execution to finish:
import { setTimeout as delay } from 'node:timers/promises';
let execution = await client.machines.executions.create({
machine_id: machine.machine_id,
command: [
'/bin/sh',
'-c',
"printf 'hello from Dedalus\\n' > /root/sdk-example.txt && cat /root/sdk-example.txt",
],
timeout_ms: 30_000,
});
const finished = new Set(['succeeded', 'failed', 'cancelled', 'expired']);
for (let attempt = 0; !finished.has(execution.status) && attempt < 120; attempt++) {
await delay(500);
execution = await client.machines.executions.retrieve({
machine_id: machine.machine_id,
execution_id: execution.execution_id,
});
}
if (!finished.has(execution.status)) {
throw new Error(`Still waiting for execution ${execution.execution_id}`);
}
const output = await client.machines.executions.output({
machine_id: machine.machine_id,
execution_id: execution.execution_id,
});
if (execution.status !== 'succeeded') {
throw new Error(output.stderr || execution.error_message || execution.status);
}
process.stdout.write(output.stdout ?? '');
Example output:
hello from Dedalus
Execution status is separate from API status. A successful API request can return an execution whose status is failed. Check the execution status before using its output.
timeout_ms limits how long the command can run inside the machine. The execution continues independently of your application's polling loop until it finishes, is cancelled, expires, or reaches that timeout. See Executions for working directories, environment variables, input, cancellation, and output limits.
Reuse the machine
Dedalus Machines are persistent. Save the machine ID to reuse the same filesystem across application sessions.
In a later application session, you can set MACHINE_ID to the saved ID and retrieve it:
import Dedalus from 'dedalus';
const savedMachineId = process.env.MACHINE_ID;
if (!savedMachineId) throw new Error('Set MACHINE_ID to your saved machine ID');
const resumedClient = new Dedalus();
const savedMachine = await resumedClient.machines.retrieve({ machine_id: savedMachineId });
The returned savedMachine contains the machine's current state.
Submit another execution with savedMachine.machine_id to continue working. Executions wake a sleeping machine automatically. Sleep preserves files on the root filesystem and resets processes and memory. See persistence for the complete lifecycle contract.
When you no longer need the machine or its files, explicitly request deletion:
await client.machines.delete({ machine_id: machine.machine_id });
Deletion removes the machine and its files. Sleep and autosleep preserve the machine for another session.
Watch lifecycle changes
watch streams machine state changes using server-sent events (SSE).
desired_state is the requested state. status.phase is the machine's current state. This example requests sleep and reads updates until the machine is sleeping:
await client.machines.sleep({ machine_id: machine.machine_id });
const stream = await client.machines.watch({ machine_id: machine.machine_id });
for await (const update of stream) {
console.log(update.status.phase);
if (update.status.phase === 'sleeping') break;
}
Each update contains a complete machine response. Break from the loop or call stream.controller.abort() to close the stream.
The stream closes when the machine reaches its desired state, reports a terminal error, or the five-minute watch limit expires. Retrieve the machine after the stream closes to confirm its current state:
const current = await client.machines.retrieve({ machine_id: machine.machine_id });
if (current.status.phase !== 'sleeping') {
throw new Error(`Sleep not complete: ${current.status.phase}`);
}
List machines
Machine lists are paginated. limit sets the number of machines requested in each page, and cursor continues from a previous page.
Use for await to fetch pages automatically:
for await (const item of client.machines.list({ limit: 20 })) {
console.log(item.machine_id);
}
To request one page at a time, inspect items and call getNextPage() only when you want another request:
const firstPage = await client.machines.list({ limit: 20 });
for (const item of firstPage.items) {
console.log(item.machine_id);
}
if (firstPage.hasNextPage()) {
const secondPage = await firstPage.getNextPage();
console.log(secondPage.items);
}
Configure requests
Client options apply to every request. Pass request options as the second argument to a resource method to override settings for that call.
| Option | What it controls |
|---|---|
apiKey | Sends the API key in the Authorization header. Defaults to DEDALUS_API_KEY. |
xApiKey | Sends a key in the alternative x-api-key authentication header. Defaults to DEDALUS_X_API_KEY. |
dedalusOrgID | Sends an organization ID with requests. Defaults to DEDALUS_ORG_ID. |
baseURL | Selects the API endpoint. Defaults to DEDALUS_BASE_URL, or https://dcs.dedaluslabs.ai when unset. |
maxRetries | Sets how many times a failed request can be retried. Defaults to 2. Set it to 0 to disable retries. |
timeout | Limits each request attempt in milliseconds. Defaults to 60000 (one minute). |
logLevel | Controls diagnostic logging. Defaults to warn. |
fetch, fetchOptions | Customize the HTTP transport, including proxy settings. |
This example disables retries for the client and enables two retries for one request:
const configuredClient = new Dedalus({ maxRetries: 0, timeout: 20_000 });
const configuredMachine = await configuredClient.machines.retrieve(
{ machine_id: machine.machine_id },
{ maxRetries: 2, timeout: 5_000 },
);
console.log(configuredMachine.machine_id);
Retries and timeouts
The SDK retries connection failures, HTTP 408, 409, 429, and 5xx responses twice by default, with increasing delays between attempts. An explicit x-should-retry response header overrides the status-based decision.
A timed-out attempt throws APIConnectionTimeoutError and can also be retried. Because timeout applies to each attempt, a call with retries can take longer than that value.
SDK methods
The SDK groups related methods under resources. All methods accept a parameter object. Methods that act on an existing machine require machine_id.
| Resource | Methods | What you can build |
|---|---|---|
client.machines | create, retrieve, update, list, sleep, wake, watch, delete | Create machines, change resources, and control their lifecycle. |
client.machines.executions | create, retrieve, list, events, output, delete | Run commands, inspect progress and output, or cancel running work. See Executions. |
client.machines.ssh | create, retrieve, list, delete | Request credentials for an SSH connection, then use the returned connection data with an SSH client. |
client.machines.terminals | create, retrieve, list, delete | Create terminal sessions, then connect to the returned WebSocket stream. |
client.machines.previews | create, retrieve, list, delete | Expose a service running on a machine through a preview URL. |
client.machines.artifacts | retrieve, list, delete | Get metadata and download URLs for files captured from executions. |
client.usage | retrieve, machineCompute, machineStorage | Inspect account usage or usage broken down by machine. |
Use the API reference for every request field and response field.
Handle errors
An API request and a command can fail independently. A successful HTTP response to executions.retrieve can describe a failed command: check execution.status, exit_code, error_message, and captured stderr before using its results.
After an HTTP timeout, use the execution ID to retrieve the existing execution. Submitting another command creates another execution. The client timeout controls HTTP waiting, execution timeout_ms controls command runtime, and machine autosleep controls idle compute.
Failed HTTP requests throw a subclass of Dedalus.APIError. Inspect status, headers, and error for the HTTP status, response headers, and error body. Connection failures have no HTTP response.
Catch errors where your application can report or handle them. This example reports diagnostic fields and rethrows the error:
try {
await client.machines.retrieve({ machine_id: machine.machine_id });
} catch (error) {
if (error instanceof Dedalus.APIError) {
console.error(error.status, error.name);
console.error(error.error);
}
throw error;
}
The error body can include error_code, message, and retryable.
| HTTP status | Error type |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | UnprocessableEntityError |
| 429 | RateLimitError |
| 5xx | InternalServerError |
| No response | APIConnectionError |
Other unsuccessful status codes use the base APIError type.
Advanced client options
Work with types
The SDK exports types for request parameters and response fields. Editor autocomplete and hover documentation explain each field. Method calls infer response types, and you can add explicit annotations where they help your application.
const params: Dedalus.MachineCreateParams = {
vcpu: 1,
memory_mib: 4096,
storage_gib: 10,
autosleep: '5m',
};
const created: Dedalus.Machine = await client.machines.create(params);
console.log(created.machine_id);
These types provide compile-time checks and editor documentation for the API.
Inspect HTTP responses
Use .withResponse() to receive both the parsed result and the underlying Fetch Response. It resolves after the body has been read and parsed.
const { data, response } = await client.machines
.retrieve({ machine_id: machine.machine_id })
.withResponse();
console.log(response.status);
console.log(data.machine_id);
Example output for a successful retrieval:
200
dm-example
Use .asResponse() to parse or stream the body yourself. It resolves when successful response headers arrive and leaves the body unread. Access headers through response.headers.get(name).
This example reads the raw response body as it arrives:
const rawResponse = await client.machines
.retrieve({ machine_id: machine.machine_id })
.asResponse();
const responseBody = rawResponse.body;
if (!responseBody) throw new Error('Response has no body');
const decoder = new TextDecoder();
for await (const chunk of responseBody) {
process.stdout.write(decoder.decode(chunk, { stream: true }));
}
process.stdout.write(decoder.decode());
Logging
Set DEDALUS_LOG or the client option logLevel. The client option takes precedence over the environment variable.
| Level | Messages included |
|---|---|
debug | Requests, responses, and all lower-verbosity messages. |
info | Information, warnings, and errors. |
warn | Warnings and errors. This is the default. |
error | Errors only. |
off | No SDK logs. |
The default logger is globalThis.console. Pass a compatible logger through the logger client option to send messages elsewhere. logLevel still filters messages before they reach that logger.
Report logger incompatibilities in the SDK issue tracker.
Customize HTTP requests
The SDK uses the runtime's global fetch. Replace it globally or pass a compatible implementation through the client option fetch. Set fetchOptions to customize request options while keeping the current transport.
Request-level fetchOptions override client-level options. Proxy configuration depends on the runtime:
| Runtime | Configuration |
|---|---|
| Node.js | Install undici, create a ProxyAgent, and pass it as fetchOptions.dispatcher. See Undici proxy configuration. |
| Bun | Set fetchOptions.proxy to the proxy URL. See Bun proxies. |
| Deno | Create a client with Deno.createHttpClient({ proxy: { url } }), then pass it as fetchOptions.client. See Deno HTTP clients. |
Requests beyond the generated types
client.get, client.post, and the other HTTP methods let you call endpoints that do not yet have resource methods. They retain client settings such as authentication and retries.
Use the query, body, and headers request options for additional arguments. Extra fields placed directly in resource parameters go into the query for GET requests and the body for other methods.
For a field absent from the generated types, a targeted @ts-expect-error can bypass the type error. A type assertion can also describe an extra response property.
Supported runtimes
The SDK supports TypeScript 4.9 or later and the following runtimes:
- Node.js 20 or later while the release is supported.
- Current Chrome, Firefox, Safari, and Edge browsers.
- Deno 1.28.0 or later and Bun 1.0 or later.
- Cloudflare Workers and Vercel Edge Runtime.
- Jest 28 or later with the
nodeenvironment. - Nitro 2.6 or later.
Versioning
Review the release notes before upgrading. If you depend on an internal interface, or find a compatibility issue, report it in the SDK issue tracker.
