Go SDK

Use the Dedalus Go 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 Go 1.22 or later. Run this command inside a Go module:

go get github.com/dedalus-labs/dedalus-go

Authenticate

Create an API key in the Dedalus Dashboard, then set it in the environment:

export DEDALUS_API_KEY="your-api-key"

dedalus.NewClient() 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:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/dedalus-labs/dedalus-go"
)

func main() {
	client := dedalus.NewClient()
	machine, err := client.Machines.New(context.Background(), dedalus.MachineNewParams{
		CreateParams: dedalus.CreateParams{
			VCPU:       1,
			MemoryMiB:  4096,
			StorageGiB: 10,
			Autosleep:  dedalus.String("5m"),
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(machine.MachineID)
}

Use the saved machine.MachineID 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 ExecutionID before the command finishes. Use that ID to check the command's status and retrieve its output.

This function writes a file, reads it back, and waits for the execution to finish:

package examples

import (
	"context"
	"fmt"
	"time"

	"github.com/dedalus-labs/dedalus-go"
)

func runCommand(ctx context.Context, client dedalus.Client, machineID string) error {
	execution, err := client.Machines.Executions.New(ctx, dedalus.MachineExecutionNewParams{
		MachineID: machineID,
		ExecutionCreateParams: dedalus.ExecutionCreateParams{
			Command: []string{
				"/bin/sh",
				"-c",
				"printf 'hello from Dedalus\\n' > /root/sdk-example.txt && cat /root/sdk-example.txt",
			},
			TimeoutMs: dedalus.Int(30_000),
		},
	})
	if err != nil {
		return err
	}
	finished := map[dedalus.ExecutionStatus]bool{
		"succeeded": true, "failed": true, "cancelled": true, "expired": true,
	}
	for attempt := 0; !finished[execution.Status] && attempt < 120; attempt++ {
		time.Sleep(500 * time.Millisecond)
		execution, err = client.Machines.Executions.Get(ctx, dedalus.MachineExecutionGetParams{
			MachineID: machineID, ExecutionID: execution.ExecutionID,
		})
		if err != nil {
			return err
		}
	}
	if !finished[execution.Status] {
		return fmt.Errorf("still waiting for execution %s", execution.ExecutionID)
	}
	output, err := client.Machines.Executions.Output(ctx, dedalus.MachineExecutionOutputParams{
		MachineID: machineID, ExecutionID: execution.ExecutionID,
	})
	if err != nil {
		return err
	}
	if execution.Status != "succeeded" {
		return fmt.Errorf("%s: %s: %s", execution.Status, execution.ErrorMessage, output.Stderr)
	}
	fmt.Print(output.Stdout)
	return nil
}

Example output when the function is called:

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.

TimeoutMs 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, retrieve the saved ID:

machine, err := client.Machines.Get(ctx, dedalus.MachineGetParams{MachineID: savedMachineID})
if err != nil {
	return err
}
fmt.Println(machine.MachineID)

The returned machine contains the machine's current state.

Submit another execution with machine.MachineID 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:

_, err = client.Machines.Delete(ctx, dedalus.MachineDeleteParams{MachineID: machine.MachineID})

Deletion removes the machine and its files. Sleep and autosleep preserve the machine for another session.

Watch lifecycle changes

WatchStreaming streams machine state changes using server-sent events (SSE).

DesiredState is the requested state. Status.Phase is the machine's current state. This function requests sleep and reads updates until the machine is sleeping:

package examples

import (
	"context"
	"fmt"

	"github.com/dedalus-labs/dedalus-go"
)

func sleepMachine(ctx context.Context, client dedalus.Client, machineID string) error {
	if _, err := client.Machines.Sleep(ctx, dedalus.MachineSleepParams{MachineID: machineID}); err != nil {
		return err
	}
	stream := client.Machines.WatchStreaming(ctx, dedalus.MachineWatchParams{MachineID: machineID})
	defer stream.Close()
	for stream.Next() {
		phase := stream.Current().Status.Phase
		fmt.Println(phase)
		if phase == "sleeping" {
			break
		}
	}
	if err := stream.Err(); err != nil {
		return err
	}
	current, err := client.Machines.Get(ctx, dedalus.MachineGetParams{MachineID: machineID})
	if err != nil {
		return err
	}
	if current.Status.Phase != "sleeping" {
		return fmt.Errorf("sleep not complete: %s", current.Status.Phase)
	}
	return nil
}

Each update contains a complete machine response. defer stream.Close() closes the open stream when the function returns.

The stream closes when the machine reaches its desired state, reports a terminal error, or the five-minute watch limit expires. The final retrieve confirms the machine's current state.

List machines

Machine lists are paginated. Limit sets the number of machines requested in each page, and Cursor continues from a previous page.

Use ListAutoPaging to fetch pages automatically:

func listMachines(ctx context.Context, client dedalus.Client) error {
	iter := client.Machines.ListAutoPaging(ctx, dedalus.MachineListParams{Limit: dedalus.Int(20)})
	for iter.Next() {
		fmt.Println(iter.Current().MachineID)
	}
	return iter.Err()
}

To request one page at a time, inspect Items and call GetNextPage() only when you want another request. A nil page means there are no more pages.

func listTwoPages(ctx context.Context, client dedalus.Client) error {
	firstPage, err := client.Machines.List(ctx, dedalus.MachineListParams{Limit: dedalus.Int(20)})
	if err != nil {
		return err
	}
	for _, item := range firstPage.Items {
		fmt.Println(item.MachineID)
	}
	secondPage, err := firstPage.GetNextPage()
	if err != nil {
		return err
	}
	if secondPage != nil {
		fmt.Println(secondPage.Items)
	}
	return nil
}

Configure requests

Pass options to dedalus.NewClient(...) for every request, or after a method's parameter struct for one request.

OptionWhat it controls
option.WithAPIKeySends the API key in the Authorization header. Defaults to DEDALUS_API_KEY.
option.WithXAPIKeySends a key in the alternative x-api-key authentication header. Defaults to DEDALUS_X_API_KEY.
option.WithDedalusOrgIDSends an organization ID with requests. Defaults to DEDALUS_ORG_ID.
option.WithBaseURLSelects the API endpoint. Defaults to DEDALUS_BASE_URL, or https://dcs.dedaluslabs.ai when unset.
option.WithMaxRetriesSets how many times a failed request can be retried. Defaults to 2. Set it to 0 to disable retries.
option.WithRequestTimeoutSets a timeout for each attempt. Requests have no SDK timeout by default.
option.WithHTTPClientReplaces the HTTP client.
option.WithMiddlewareAdds logic around requests.
option.WithDebugLogLogs HTTP requests and responses for debugging.

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 context deadline limits the entire call, including retries. option.WithRequestTimeout limits each attempt separately. Neither adds a timeout unless you configure it.

SDK methods

The SDK groups related methods under services. Every method accepts a context and a parameter struct. Check its returned error before using the result.

ServiceMethodsWhat you can build
client.MachinesNew, Get, Update, List, ListAutoPaging, Sleep, Wake, WatchStreaming, DeleteCreate machines, change resources, and control their lifecycle.
client.Machines.ExecutionsNew, Get, List, ListAutoPaging, Events, EventsAutoPaging, Output, DeleteRun commands, inspect progress and output, or cancel running work. See Executions.
client.Machines.SSHNew, Get, List, ListAutoPaging, DeleteRequest credentials for an SSH connection, then use the returned connection data with an SSH client.
client.Machines.TerminalsNew, Get, List, ListAutoPaging, DeleteCreate terminal sessions, then connect to the returned WebSocket stream.
client.Machines.PreviewsNew, Get, List, ListAutoPaging, DeleteExpose a service running on a machine through a preview URL.
client.Machines.ArtifactsGet, List, ListAutoPaging, DeleteGet metadata and download URLs for files captured from executions.
client.UsageGet, MachineCompute, MachineStorageInspect 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 nil error from Executions.Get can accompany a failed command: check execution.Status, ExitCode, ErrorMessage, 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. Request deadlines control HTTP waiting, execution TimeoutMs controls command runtime, and machine Autosleep controls idle compute.

Unsuccessful HTTP responses return *dedalus.Error. Use errors.As to inspect StatusCode and RawJSON():

var apiErr *dedalus.Error
if errors.As(err, &apiErr) {
	fmt.Println(apiErr.StatusCode)
	fmt.Println(apiErr.RawJSON())
}

In v0.4.0, error-body fields such as error_code, message, and retryable are not named Go struct fields. Decode RawJSON() into your own type when your application needs them.

Advanced client options

Work with request and response fields

Optional primitive request fields use param.Opt[T]. Constructors such as dedalus.String("5m") and dedalus.Int(20) set them. Use param.Null[T]() only when the API permits an explicit JSON null.

Response structs expose values directly and keep JSON metadata in a JSON field. Check response.JSON.Field.Valid() before interpreting an optional field's zero value. Use response.RawJSON() for the original response.

Inspect HTTP responses

Use option.WithResponseInto when you need the HTTP status or headers with the parsed result:

var response *http.Response
machine, err := client.Machines.Get(
	ctx,
	dedalus.MachineGetParams{MachineID: machineID},
	option.WithResponseInto(&response),
)
if err != nil {
	return err
}
fmt.Println(response.StatusCode)
fmt.Println(machine.MachineID)

Logging

Pass option.WithDebugLog(nil) to use the default debug logger, or provide a log.Logger.

Customize HTTP requests

Use option.WithHTTPClient to supply an HTTP client with your proxy, transport, or connection settings. Use option.WithMiddleware to add application-specific timing or diagnostics around requests.

Requests beyond the generated types

client.Get, client.Post, and the other HTTP methods let you call endpoints that do not have service methods. Client options such as authentication and retries still apply.

Use option.WithQuerySet for extra query parameters and option.WithJSONSet for body fields.

Multipart file uploads

For endpoints that accept multipart files, upload parameters use io.Reader. Wrap a reader with dedalus.File(reader, filename, contentType) to set the filename and content type.

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.