Skip to main content
@idyl-labs/sdk is the official TypeScript SDK for IDYL. It is one package with one entry point, the Idyl client, and no runtime dependencies. It runs on Node.js 22 and later, in evergreen browsers, and in Workers-class edge runtimes from a single build, using only web-standard APIs: fetch, AbortSignal, ReadableStream, TextEncoder, TextDecoder, and URL. The package ships ESM and CommonJS builds with type declarations. The SDK implements the model the Go SDK defines: the same resources, the same names, and the same wire format, held by a conformance suite against fixtures taken from the Go SDK. Use the repository and the package page as the source and API reference.

Installation

Import the package as ESM or CommonJS. Type declarations ship with both builds.

Configure a client

Everything the client needs is in one ClientConfig value. The SDK reads no environment variable, configuration file, or ambient credential store, and substitutes nothing for a field left unset. Invalid configuration throws a TypeError naming the field. credential is a CredentialSource. Its token method is called for every authenticated request rather than once at construction, so a source that refreshes or rotates a short-lived credential is adopted without rebuilding the client or disturbing clients scoped from it. Every token is opaque to the SDK: it is sent as a bearer credential and never inspected. staticToken wraps a fixed token. credentialSourceFunc adapts a function that receives the request’s AbortSignal, so a cancelled request never leaves a token exchange running.

Quick start

Create a Sandbox, wait until it is ready, and delete it:
createAndWait takes an optional second argument, { timeoutMs, pollIntervalMs, signal }. Left out, the wait allows ten minutes and polls every second. A Sandbox with neither cpu nor memory set is limited to one CPU and 1Gi of memory; setting either overrides the default. The handle it returns is an immutable snapshot of the ready Sandbox. The examples on the rest of this page use the idyl client configured here.

Scope

A client is immutable. Every scoping method returns a copy and leaves the receiver unchanged, so a broadly configured client can be narrowed per call site while the original stays valid. Copies share one transport, which is safe for concurrent use.
Direct Workload operations need only the Namespace and placement. Computer operations take a Computer name or ID through idyl.account.computers. scope reports the frozen Account, Namespace, and placement a client runs in.

Placement

Placement says where Workloads run. Values come from four constructors, are frozen, and are validated when they are built:
Resolution happens through public APIs, once per operation, and is never cached. A Fleet or Node placement is resolved to its Subnet through the Account’s deployment-target API. Every placement is then checked against the Subnets the Account may deploy to, and the request goes to the public API host advertised for that Subnet. A Fleet or Node placement also becomes the scheduling constraint on every manifest the client submits. A Subnet that cannot accept the request fails with a PlacementUnavailableError whose reason names the classification: reason is a closed vocabulary that only grows: a value is never renamed or removed, and a new value is a new case to handle. A caller that has already resolved and authorized a Subnet route can bind it instead. A bound client addresses that endpoint directly and repeats no discovery. Binding changes neither authentication, scope, nor server-side authorization:

Services

The Idyl client exposes one service per resource family. Workload services run in the client’s Namespace and resolved Subnet; Account services address the Account directly. Every Namespace-scoped service also lists its collection account-wide through listAllNamespaces, which spans every Namespace the caller is authorized to list and needs no Namespace on the client. List options are limit and offset; list responses carry items, totalCount, and snapshotCursor.

Handles and waits

Every Workload operation returns a handle, an immutable snapshot of what the platform recorded. A handle reads its own state, such as name and phase, without touching the network, and manifest() returns an independent copy of the snapshot. refresh reads the resource again and returns a new handle; update replaces the definition and returns a new handle; delete asks the platform to remove the resource and resolves once the request is accepted. The receiver is never changed.
Native options keep common workflows short, and createManifest accepts the full manifest type when the options cannot express a definition. createAndWait and createManifestAndWait submit and wait in one call:
Each handle waits on the conditions its kind can reach. A wait polls until the condition holds and returns the handle that observed it. Every wait takes a WaitOptions value, { timeoutMs, pollIntervalMs, signal }. The defaults are ten minutes and one second. A wait that ends in a state the resource cannot leave rejects with a WorkloadTerminalError carrying kind, workloadName, condition, and the platform’s explanation as detail when it supplied one. A wait that exhausts its timeout rejects with a TimeoutError DOMException, and one cancelled through signal rejects with the signal’s reason. None of those outcomes deletes anything.

Identity and discovery

Sign-in is anonymous, so it uses a separate Identity client with no Account or credential:
Every operation that needs a refresh token takes it as an argument and sends it as the bearer credential of that one request. The client holds no credential of its own. Public region and Subnet discovery needs neither an Account nor a credential. Discovery reads a Network’s metadata endpoint anonymously. The same reads are available as idyl.regions and idyl.subnets on an authenticated client, where they are anonymous too: a bearer credential never reaches the metadata endpoint.

Logs, shells, and watches

idyl.workloadSessions.logs streams a container’s logs over a WebSocket session. The SDK reserves the session, presents its one-use grant as the first message on the socket, delivers the frames to the sinks it is given in order, and closes the socket on completion or cancellation:
A sink is a WritableStream<Uint8Array> or a function called with each chunk, and one sink may serve both streams. A session that ends in an error frame rejects with WorkloadSessionError, whose code is the session’s closed vocabulary. A transport failure rejects with WorkloadSessionTransportError, whose code is the transport’s closed vocabulary and whose message is a fixed sentence. Cancelling the request signal closes the socket and rejects with the signal’s reason. The SDK speaks no SSH. openShellTransport reserves a shell session for the caller’s ephemeral key, receives a one-use certificate bound to that key, and opens the byte duplex an SSH implementation authenticates over. The caller owns the key pair and gives the SDK the public key in OpenSSH authorized-key form; the implementation that attaches owns the key, the handshake, and the host-key decision:
A watch is an ordered stream of typed events: an async iterable with a close. watchWorkloads streams Workload changes in the resolved Subnet, narrowed to the client’s Namespace when it has one. watchRuntimePods streams the observed pods of the client’s Namespace and watchRuntimeNodes the observed Nodes of its Subnet. Take a Workload snapshot before watching when a consumer needs a gap-free initial view, then pass its snapshotCursor to the matching watch: every change after the snapshot is delivered exactly once and nothing before it is redelivered. Always close a watch a consumer leaves early.
Every watch is opened before it is handed back, so a refused stream rejects there with an ApiError. Iteration ends when the platform closes the stream, rejects with LiveStreamError on an error frame, and rejects with the request signal’s reason on cancellation. Events of a type the SDK does not name are ignored, so a newer platform never ends a watch.

Errors and cancellation

A non-2xx response becomes an ApiError carrying status, the stable code when the endpoint publishes one, message, details, requestId, and retryAfterMs. Branch on the fields or on the predicates isApiStatus, isApiCode, and isNotFound. The message is written for logs and for people and is not a contract.
Failures that are not HTTP responses have their own classes, each carrying the fields to branch on: Every class sets name and preserves its cause. Invalid arguments and configuration, a malformed option, a manifest whose scope disagrees with the client’s, or a Workload operation on a client with no Namespace or placement, throw a TypeError before any request is made.
The SDK issues no automatic retries. A request that fails is returned to the caller, which knows whether the operation is safe to repeat. Every operation accepts an AbortSignal, as signal in the request options of a call or in the WaitOptions of a wait. Cancellation propagates through the credential lookup, the request, the body read, and any attached stream or session, and the operation rejects with the signal’s reason. Each request is also bounded by requestTimeoutMs, 30 seconds by default; a request that exceeds it rejects with a TimeoutError DOMException. Cancelling an operation never deletes a resource.

Manifests

A manifest is an envelope of kind, metadata, and spec, with status added by the platform on read, and the SDK ships every kind as a plain read-only type with the same field names and wire spellings as the Go SDK. Decoding is lenient on read and strict on write. decodeSubmission prepares a manifest for submission: it takes JSON text or a parsed value and refuses an unknown field, a duplicate key, a value of the wrong type, and a missing envelope field, throwing a StrictDecodeError that names the field by its full path from the document root. decodeObserved reads a manifest the platform reported: a field the SDK does not declare is carried, a value it cannot read is carried as it arrived, and a missing field reads as its zero value. Each kind’s validate* function, such as validateSandbox, holds a manifest to the contract the Go SDK enforces and throws a TypeError naming the first violation.

Compatibility

Every exported symbol and every declaration in the package’s type declarations is a long-term commitment. JSON field names are compatibility commitments shared with the Go SDK: the wire format is the Go SDK’s, held by a conformance suite that decodes and re-encodes the Go SDK’s golden fixtures at a pinned commit. New fields preserve decoding compatibility. A rename, a removal, a discriminator change, or a change to a semantic default is a versioned contract decision made in the Go SDK first and then adopted here.