Workspace

Persistent disks

Think of an additional disk as an external drive for a workspace: connect it when needed, disconnect it, and reuse its files elsewhere. A managed disk has its own ID and lifetime. Your disks can hold projects, datasets or prepared software, independently of the workspace that uses them.

Attachments currently require a stopped workspace and become available at its next boot. This is not live USB hot-plugging.

Ordinary workspaces still start with a shared base image and private writable storage. Managed disks are optional.

Catalog presets can include published software disks. Selecting a preset such as Xcode 26.6 attaches the prepared software and configures its boot setup automatically. The software belongs to Oblien, and your account gets a private reference for managing its workspace connections. It shares platform bytes across consumers; project files, preferences and build output remain private. See Images.

Ownership

Read-only access and ownership are separate. A saved copy of your own disk still belongs to your account. Published software belongs to Oblien.

Disk ownerWhat you can do
Your accountAttach, detach, copy, save, move, grow writable storage, and delete when unused
System — provided by OblienAttach read-only, detach your workspace's connection, or Copy to my disks

You cannot move, resize, save over or delete a system disk. A private copy belongs to your account and its full capacity counts toward your storage pool. Detaching system software or deleting your workspace never changes the shared source or another workspace's connection. The API enforces these rules.

Disk responses include ownership: "user" | "system" and permissions. Permissions describe the actions currently offered; workspace state, namespace and capacity checks still apply when you request an operation.

Software library

Open Disks → Software library to browse prepared software, including Xcode 26.6 with the iOS 26.5 Simulator runtime. This list is available before you create a workspace or connect any software disks.

Inside a workspace, the library shows only software compatible with its image's base OS and version. Xcode appears for compatible macOS workspaces, not Linux. The global library still lets you browse everything before creating a workspace. Desktop environments are selected separately in the Desktop panel.

const compatible = await client.disks.listSoftware({ workspace_id: workspace.id });

The API equivalent is GET /disks/library?workspace_id=WORKSPACE_ID. Image metadata includes base_os with family, distribution and version; software lists its compatible_os and supported images. Unknown or incompatible images cannot attach software simply by guessing its ID.

  • Attach read-only connects the software to a compatible stopped workspace.
  • Create workspace selects its image catalog preset, including automatic boot setup and readiness checks.
  • The Disks view shows your connections, the disk owner, and available actions.
const software = await client.disks.listSoftware();
const tools = software.find(disk => disk.label === 'Xcode 26.6')!;

const attached = await client.disks.attachSoftware(tools.id, {
  workspace_id: 'STOPPED_MAC_WORKSPACE_ID',
  idempotency_key: 'connect-developer-tools',
});
console.log(attached.disk!.ownership); // system

The server supplies the published read-only mount settings and creates an account/namespace reference as needed. No private copy is downloaded per workspace. Software must be published on the workspace's host; use its catalog preset to create a compatible workspace when it is unavailable on an existing one.

Attaching a disk makes its files available; it does not run its installers. For Xcode, the catalog preset handles activation automatically. When attaching to an existing Mac, use the activation steps below to register the tools in that workspace. Projects, caches and Simulator devices remain in private storage.

Dashboard

Open Disks in the dashboard to create and manage storage. Each disk shows its capacity, state, and attached workspaces. Open a workspace's Disks tab to attach storage or retain its root.

  • Create disk creates empty ext4 storage, or an unformatted raw block device.
  • Attach makes a disk available at the next boot. An ext4 disk can mount automatically at a directory such as /data/project.
  • Move transfers a disk to another stopped workspace, preserving its ID and contents.
  • Copy creates another writable disk with independent changes.
  • Save creates an immutable copy. Saved data disks can have multiple read-only consumers.
  • Attach as software layer adds a saved Linux root's prepared files to a compatible workspace, underneath its private writable storage.
  • Fork root creates a fresh writable root on a saved root. Prepared files remain shared; each fork keeps its own changes.
  • Retain root gives an existing workspace's private root a persistent disk ID. It then survives workspace deletion.

To reuse a detached root, open its menu in Disks and select Create workspace. During ordinary workspace creation, choose an image from the catalog; add data disks and software layers under Resources → Additional disks. CPU and memory use the usual workspace settings. A reused root keeps its recorded image and disk capacity.

Stop affected workspaces before attaching, detaching, moving, retaining, or copying their writable storage. A paused or hibernated workspace is not cold-stopped. For a hibernated workspace, restore it and stop it, or choose Discard saved execution before changing storage. Discarding saved execution removes saved RAM; files remain, and the next start is a cold boot.

Create and attach a data disk

import Oblien from 'oblien';

const client = new Oblien({ token: process.env.OBLIEN_TOKEN });
const { disk } = await client.disks.create({
  name: 'Project files',
  namespace: 'my-project',
  size_mb: 4096,
  idempotency_key: 'project-files-v1',
});

const workspace = await client.workspaces.create({
  namespace: 'my-project',
  image: 'oblien/ubuntu:24.04',
  cpus: 1,
  memory_mb: 1024,
  disks: [{ disk_id: disk!.id, mount_path: '/data/project' }],
});

If your SDK does not expose client.disks, use the REST endpoints below until you update it.

A disk and its workspace must have the same account and namespace. Mutable disks have one writable attachment. Adding a data disk never formats it or merges its files into the root filesystem. Root composition is a separate, explicit software-layer option.

Without mount_path, access the device inside Linux at /dev/oblien/disks/DISK_ID. Raw disks always use block-device access; partitioning and filesystems are your responsibility. Device letters such as /dev/vdc are not stable identifiers.

Automatic mounts must be clean absolute paths outside system directories. Mounts cannot overlap. Existing files at the mount directory are hidden while it is mounted, so use a dedicated directory.

Move between workspaces

await client.workspace('SOURCE_WORKSPACE').stop();
await client.workspace('TARGET_WORKSPACE').stop();

await client.disks.move('DISK_ID', {
  source_workspace_id: 'SOURCE_WORKSPACE',
  target_workspace_id: 'TARGET_WORKSPACE',
  mount_path: '/data/project',
  idempotency_key: 'move-project-files-v1',
});

await client.workspace('TARGET_WORKSPACE').start();

A move can take longer when it transfers data between hosts. The target becomes writable only after the move commits. While the operation is pending, affected disks and workspaces stay reserved. A failed move keeps those reservations so it can safely retry.

Selected disks constrain where a new workspace can start. All disks selected at creation must be available on the same host. Attach or move disks to a common stopped workspace first if necessary. A saved disk already shared by readers stays on their host; copy it for independent placement.

Reuse a prepared root

Prepare a workspace normally, install tools, and save any settings you want future workspaces to inherit. Then:

const source = client.workspace('PREPARED_WORKSPACE');
await source.stop();

const retained = await source.disks.retainRoot({ name: 'Prepared root' });
const saved = await client.disks.save(retained.disk!.id, { name: 'Prepared environment' });
const fork = await client.disks.fork(saved.disk!.id, {
  name: 'New project root',
  size_mb: 4096,
});

const workspace = await client.workspaces.create({
  namespace: 'my-project', // Same namespace as the prepared source.
  root_disk_id: fork.disk!.id,
  cpus: 1,
  memory_mb: 1024,
});

The saved files are shared read-only. Writes in the new workspace belong to its private root and cannot change the saved root or another fork. Its size_mb is the capacity of that private writable storage.

Saving and copying preserve files and the root's compatible boot definition. They do not preserve running processes. Use workspace Hibernate / Restore to save and continue execution instead.

To reuse the exact retained root, stop or delete its old workspace, detach the root if needed, and create a workspace with root_disk_id. This preserves its existing files and uses a cold boot. A data disk is not a bootable root.

Root composition supports up to four saved layers. Reuse an earlier saved root when you reach that depth. A referenced saved root cannot be deleted until its dependent roots are deleted.

Saved storage contains your prepared files and configuration, including any credentials you leave there. Prepare reusable roots without project secrets you do not want copied to their consumers.

Share installed Linux software

Prepare a workspace using the same base image as its consumers, install the packages once, stop it, retain its root, and save that root. Attach the saved disk as a software layer:

const workspace = await client.workspaces.create({
  namespace: 'my-project',
  image: 'oblien/ubuntu:24.04',
  memory_mb: 2048,
  disk_size_mb: 4096,
  disks: [{ disk_id: 'SAVED_ROOT_DISK_ID', role: 'layer', read_only: true }],
  // For a layer prepared using the workspace Desktop installer:
  config: { desktop: { enabled: true } },
});

The prepared files are visible at their usual paths. Writes go into each workspace's private root. A prepared desktop layer starts its desktop without downloading its packages again. Desktop access remains optional and uses the normal Desktop tab.

Software layers must use the exact same base image contents, not just a similar distribution name. Later layers take precedence over earlier ones; workspace writes take precedence over all saved layers. Attaching a layer does not run a package manager or reconcile conflicting packages, so prepare and test the intended combination together.

Stop a workspace before changing layers. Detaching a layer removes its underlying files, but does not undo files or configuration already written into the private root. Retaining or saving a composed root keeps references to its required layers.

Attach a disk to macOS or another runtime

Images can advertise runtimes that accept raw disks. Choose Runtime when adding a raw disk, or provide its target in the SDK:

const mac = await client.workspaces.create({
  namespace: 'my-project',
  image: 'oblien/macos-tahoe:26.6.2',
  cpus: 4,
  memory_mb: 16384,
  disk_size_mb: 16384,
});

const { disk } = await client.disks.create({
  name: 'Mac software preparation',
  namespace: 'my-project',
  format: 'raw',
  size_mb: 65536,
});

await client.workspace(mac.id).stop();
await client.disks.attach(disk!.id, { workspace_id: mac.id, target: 'macos' });
await client.workspace(mac.id).start();

The disk appears natively inside macOS. Identify it by its hardware serial, which matches the disk ID, before formatting an empty disk as APFS. Formatting is explicit and destructive; attachment never formats it for you. Formatted, unlocked APFS volumes are mounted before the Mac reports ready. The disk has its own capacity, separate from workspace root storage.

Creating the workspace first selects a host that can run its image; attaching afterward can transfer the disk there. You can also supply disks during creation when the image and runtime are available at the disks' location.

Install shareable tools onto that dedicated volume, stop the preparation workspace, and save the disk. Fresh Macs can attach the saved disk with { disk_id, target: 'macos', read_only: true }. Each reader uses the same immutable volume; macOS rejects writes to it. Use Copy to get an independent writable disk, or Move to transfer a writable disk to another stopped workspace.

Keep user settings, build caches, build outputs and simulator device data in private workspace storage. A saved whole Mac root is a complete environment, not a portable application-only layer. Applications must support their installation location and read-only operation. Disk attachment alone does not install or register them.

Raw copies preserve partition and filesystem identities. Avoid attaching multiple copies with the same APFS identity to one Mac. Choose distinct volume names for separately prepared software disks. Use target: 'local' for the outer Linux runtime. Other targets require support from that image's runtime provider; unknown targets are rejected.

Prepared Xcode and iOS tools

For an existing Mac, attach the library disk, start the workspace, then run this in its Mac terminal to activate the tools for the current boot:

/bin/bash /Volumes/OblienXcode26_6/tools/activate.sh
/bin/bash /Volumes/OblienXcode26_6/tools/ready.sh

Keep the activation command as a boot workload for future starts. Selecting Xcode 26.6 in the catalog configures that workload and readiness automatically.

A prepared software disk can provide Xcode, an iOS Simulator runtime and the Metal compiler to several Macs. Attach your saved disk read-only and add its activation command as a boot workload. A readiness check keeps the workspace preparing until those tools are available:

const volume = '/Volumes/OblienXcode26_6';
const mac = await client.workspaces.create({
  image: 'oblien/macos-tahoe:26.6.2',
  cpus: 8,
  memory_mb: 24576,
  disk_size_mb: 16384,
  disks: [{ disk_id: savedDiskId, target: 'macos', read_only: true }],
  config: {
    workloads: [{
      name: 'developer-tools',
      target: 'macos',
      restart_policy: 'never',
      command: ['/bin/bash', `${volume}/tools/activate.sh`],
    }],
    ready_check: {
      target: 'macos',
      timeout_seconds: 1800,
      command: ['/bin/bash', `${volume}/tools/ready.sh`],
    },
  },
}, { timeoutMs: 2_100_000 });

Use the volume path and activation commands supplied with your prepared disk. The disk must belong to your account and namespace; a disk ID from another account cannot be attached. CPU, memory and private storage use the usual workspace settings.

Allow extra time on a fresh workspace for macOS to verify Xcode and initialize its developer services. The example allows up to 30 minutes for that first preparation; this is a deadline, not a fixed delay. Use wait_ready: false to follow creation progress without holding the SDK call. Later boots reuse the workspace's private registration state.

Starting an iOS Simulator can take more than ten minutes on this Intel VM, including after a cold workspace restart. New devices also perform their own data migration. Toolchain readiness means the developer tools are available; it does not mean a Simulator device has already been booted. Reuse your workspace's private Simulator devices for repeated runs, but allow time for cold startup.

After preparation, xcodebuild, Swift and Simulator commands run inside macOS through the normal terminal and runtime API. Desktop access is optional. The software and a compatible prepared system cache can be shared; each workspace keeps its own projects, DerivedData, Simulator devices and app data. Device signing and App Store submission use your own Apple development team and private signing credentials.

For Simulator commands and UI tests launched by the root runtime, enter the developer account's macOS launch context. This keeps the app independent of the calling job's session and does not enable remote desktop access:

launchctl asuser "$(id -u oblien)" sudo -H -u oblien xcrun simctl list

Prepare software on a disposable Mac. Publish only the application payload copied into a clean software filesystem, then remove the signed-in preparation workspace and staging storage. Never save its browser profile, Keychain or signed-in root as a shared layer.

Progress and retries

REST mutations return an operation ID immediately by default. Poll GET /disks/operations/OPERATION_ID until state is done or failed. States are queued, running, done, and failed; phase gives more detail while work proceeds.

The SDK waits using short status requests by default. Set wait_ready: false to receive the operation immediately:

const accepted = await client.disks.clone('DISK_ID', {
  name: 'Independent copy',
  wait_ready: false,
  idempotency_key: 'project-copy-v1',
});

const completed = await client.disks.wait(accepted.operation.id, {
  timeoutMs: 30 * 60 * 1000,
  // signal: abortController.signal,
});

// After a failed operation, retry that same operation:
// await client.disks.retry(accepted.operation.id);

A timeout or aborted wait stops waiting; the disk operation continues. Keep its operation ID and resume polling. If a mutation's response was lost, resend the same request with the same idempotency_key (or Idempotency-Key header). Reusing a key with different inputs returns a conflict.

REST also accepts wait_ready: true. For large copies and transfers, polling avoids holding a long HTTP request. Retry a failed operation through its retry endpoint before requesting a different mutation on the reserved disk. Stopping a workspace and discarding saved execution remain available for recovery.

Capacity and retention

Disk capacity counts against the account storage pool even when detached. An attached managed root is counted once, alongside the workspace's normal resource allocation. Saved disks count once regardless of how many workspaces read them. Copies and private forks have their own capacity; saved layers they depend on remain allocated too.

Published catalog software is the exception: a disk with a library reference shares platform-provided bytes and does not consume the private disk pool. Copying it creates ordinary private storage, which does consume that capacity. System references cannot be deleted through the customer API. Detach a workspace to remove its connection. Library references stay on hosts containing the published artifact; make a private copy to transfer the software to another host yourself.

Capacity and allocated bytes are different: an empty sparse disk can have a large capacity while using little physical storage. File copies and transfers take time proportional to the data involved; arbitrary large disks are not promised to clone instantly.

Grow detached mutable disks with client.disks.resize(id, { size_mb }). Grow an attached root through Settings → Resources, or workspace.resources.update({ disk_size_mb }); that operation coordinates stopping and restarting the workspace if necessary. Shrinking managed disks and resizing immutable disks are unsupported.

Your additional disks and retained roots survive workspace deletion. Delete them explicitly when no workspace or saved root still needs them. System software remains in the shared library. Unretained ordinary roots retain their existing disposable behavior.

REST endpoints

All paths are relative to the API base URL and require normal API authentication. Account and namespace credentials can manage disks. Workspace credentials can list their own attachments.

Method and pathPurpose
GET /disks?namespace=SLUGList accessible disks
GET /disks/libraryBrowse system software without creating disk references
POST /disks/library/:softwareId/attachConnect published software; workspace_id, optional idempotency_key, wait_ready
POST /disksCreate: name, size_mb, optional format, namespace
GET /disks/:diskIdRead disk state and attachments
DELETE /disks/:diskIdDelete an unreferenced, detached disk
POST /disks/:diskId/cloneIndependent writable copy; optional name
POST /disks/:diskId/saveImmutable copy; optional name
POST /disks/:diskId/forkPrivate root over a saved root: size_mb, optional name
POST /disks/:diskId/resizeGrow a detached mutable disk: size_mb
POST /disks/:diskId/attachworkspace_id, optional mount_path, read_only, role, target
POST /disks/:diskId/detachworkspace_id
POST /disks/:diskId/movesource_workspace_id, target_workspace_id, optional mount_path
GET /disks/workspace/:workspaceIdList a workspace's disks and pending operation
POST /disks/workspace/:workspaceId/rootRetain its root; optional name
DELETE /disks/workspace/:workspaceId/saved-executionDiscard saved RAM before a cold storage change
GET /disks/operations/:operationIdRead progress or failure
POST /disks/operations/:operationId/retryRetry the existing failed operation

Create a workspace with config.root_disk_id and/or config.disks in the REST create body. The SDK also accepts these as top-level convenience options. Never provide host filesystem paths.

Current limits

  • Up to eight managed disks per workspace, including a managed root.
  • Mutable storage has one writer. Shared data must be an immutable saved disk attached with read_only: true.
  • Attach and detach happen before boot; live hotplug is unsupported.
  • ext4 automatic mounts target the outer Linux runtime. Use a raw disk and an advertised runtime target for nested guests such as macOS.
  • Additional software layers use saved ext4 roots with compatible base contents. This is not an arbitrary cross-distribution package merge.
  • Disks do not change workspace hibernation capabilities. Storage copy/save preserves files; Hibernate / Restore preserves supported execution state.

See sleep and wake for saving execution and guest runtimes for runtime selection.